Skip to main content

Menu

System onlineAI Tutorials / 2026

AI Tutorials
Knowledge search

Recent · Popular · Tutorials

  • dart3 tutorials
  • java1 tutorial
  • Javascript3 tutorials
  • Node.js Introduction2 tutorials
  • dartdart · General · In depth
  • JavascriptJavascript · Advanced · In depth
  • JavascriptJavascript · Advanced · Mid
  • JavascriptJavascript · Advanced · Short

Find a tutorial

Tell the assistant what you want to learn. It suggests published chapters.

For example: “I want to learn Rust ownership from the start.” The conversation is kept only in this tab.

0 / 2,000 characters

Enter to send, Shift+Enter for a new line. Do not include personal information.

Chapter 16

Asynchrony with Callbacks

This chapter assumes you are comfortable with functions, closures, `this` binding, the call stack, and array iteration methods from earlier chapters. We work in Node.js and the browser interchangeably, calling out runtime differences where they matter. `async_callbacks` sits between Functions and Pr
Level: AdvancedLength: In depth5 min read

Read at another depth

Asynchrony with Callbacks

This chapter assumes you are comfortable with functions, closures, this binding, the call stack, and array iteration methods from earlier chapters. We work in Node.js and the browser interchangeably, calling out runtime differences where they matter. async_callbacks sits between Functions and Promises in the topic plan, so we treat callbacks as an execution model in their own right, not merely as a historical stepping stone to await.

Why JavaScript is single-threaded, and what that forces

JavaScript runs your code on one call stack. There is no thread suspension: while a function is executing, nothing else in your program runs. This eliminates entire classes of race conditions on shared data, but it creates a hard constraint — a long-running computation blocks everything, including UI rendering and I/O processing.

The consequence is that JavaScript's I/O model is cooperative and event-driven. You never "wait" for the network or the filesystem. Instead you hand the runtime a function — the callback — and the runtime invokes it later, when the result is ready. The runtime re-enters your code from the event loop, which we cover in depth in its own chapter; here we focus on how callbacks shape program structure and failure handling.

Code example in javascript
// Node.js: asynchronous file read with the error-first callback convention
const fs = require("fs");

fs.readFile("config.json", "utf8", (err, data) => {
  if (err) {
    console.error("read failed:", err.code); // e.g. ENOENT
    return;
  }
  console.log("parsed:", JSON.parse(data));
});

console.log("read dispatched"); // logs first

Note the ordering: the log at the bottom prints first. readFile returns immediately; the callback is queued and invoked when the operating system completes the read.

Callback conventions you must know

Two decades of callback use crystallized into conventions that surviving APIs still follow. Internalize them; you will read this style in any mature codebase.

  1. Error-first signature: Node-style callbacks take (err, result), with err being null or undefined on success. Forgetting the if (err) return guard is the classic defect: execution falls through and reads a garbage result.
  2. Success-only signatures: Browser DOM events (onclick, addEventListener) pass only the event. There is no failure channel — the handler is invoked on the trigger, and errors inside it become unhandled exceptions routed to window.onerror.
  3. Node promises vs. callback APIs: Modern Node exposes both (fs/promises vs fs). Callback APIs remain the substrate; promise APIs are layered over them.

Deferred invocation: setTimeout is not "sleep"

A common error is treating setTimeout(fn, 0) as a way to run "next". It schedules a macrotask with at least the given delay — the delay is a floor, not a guarantee. If you need ordering with promise callbacks, timeouts are unreliable; microtasks (promise reactions) always run before the next macrotask. We defer the full scheduling model to the Event Loop chapter, but the takeaway here is: callbacks impose no ordering guarantee beyond "after the current stack unwinds", and timeout callbacks are not precisely timed.

Inversion of control

The defining property of callbacks is inversion of control: you hand your function to a callee you do not control, and it decides whether, when, and how many times to call it. Every callback defect traces back to one of these unmet assumptions:

  • Called zero times: the callee hit an error path without invoking your callback, so your code never resumes. Your program hangs.
  • Called more than once: double-invocation silently corrupts state or double-charges a payment. This is the most dangerous failure because it looks like success.
  • Called never or forever later: there is no cancellation in the callback model. A dropped callback is an unrecoverable leak of any resources the closure retains (see Memory chapter).
  • Called synchronously: an API that invokes your callback immediately is nearly always a bug source, because it inverts the ordering guarantees of the surrounding code.
Code example in javascript
function once(fn) {
  let called = false;
  let result;
  return (...args) => {
    if (called) return result;
    called = true;
    result = fn(...args);
    return result;
  };
}
// Defensive wrapper: guarantees single invocation regardless of caller behavior.

Defensive wrappers like this exist because you cannot trust the callee's discipline. Promises solve this class of problems structurally — a promise settles exactly once — which is precisely why the ecosystem migrated.

Callback hell: what actually goes wrong

"Nesting" is not the real problem — indentation is cosmetic. The real problems are, in order of severity:

  1. Error handling must be threaded manually through every level. There is no propagation mechanism: each callback must re-check and re-forward err.
  2. Parallel work is awkward. Running N operations concurrently and combining results requires manual counting, with all its race-prone bookkeeping.
  3. Composition destroys the stack. Errors thrown asynchronously lose their originating stack; you get a stack fragment from the event-loop dispatch, not the logical call site.
Code example in javascript
function getUser(id, cb) {
  db.query("SELECT * FROM users WHERE id = ?", [id], (err, rows) => {
    if (err) return cb(err);          // manual propagation, level 1
    if (!rows.length) return cb(new Error("not found"));
    getPermissions(rows[0], (err, perms) => {
      if (err) return cb(err);        // manual propagation, level 2
      cb(null, { user: rows[0], perms });
    });
  });
}

Diagram

Sequence diagram showing the main script calling an async API, which returns control immediately; the event loop later delivers the result by invoking the callback.
100%
Callback flow: the main script returns immediately; the runtime invokes the callback later via the event loop.
Show diagram source
sequenceDiagram
    participant S as Main script
    participant R as Runtime / event loop
    participant IO as Async I/O (OS)
    S->>IO: readFile(path, cb)
    IO-->>S: returns immediately
    Note over S: stack unwinds; event loop runs other work
    IO->>R: read complete
    R->>S: invokes cb(err, data)
    Note over S: callback executes as a fresh stack frame

Trade-offs: when callbacks are still the right tool

Callbacks are not obsolete. Decision criteria:

  • Event handlers (addEventListener, streams' data/end, requestAnimationFrame): a callback is the native idiom. A promise models a single future value; a recurring event is not one, and wrapping it in one is a modeling error.
  • Signals and one-off notifications in environments without promise support (embedded JS engines, older tooling).
  • Everything with a success/failure shape: prefer promises or async/await. They give you single-settlement semantics, propagation via try/catch and catch(), and composability.
ConcernCallbacksPromises/await
Settlement guaranteeNone — callee disciplineExactly once, by construction
Error propagationManual, per-levelAutomatic along the chain
Parallel compositionManual counters`Promise.all`
Recurring eventsNatural fitWrong tool
CancellationNone (leaks closures)`AbortSignal`

Callbacks and closures: the retention trap

A callback that outlives its creating scope is a closure held by the callee. Everything it captures — including large objects captured transitively — stays alive as long as the callee holds it. A listener registered on a long-lived object and never removed retains its entire scope chain. This is the dominant JavaScript memory-leak pattern; the Memory chapter revisits it with heap-analysis techniques.

What carries forward

Two ideas from this chapter are load-bearing for everything ahead. First, an asynchronous callback executes as a fresh stack frame dispatched from the event loop — synchronous code has fully unwound by then, which is why throw inside a callback cannot be caught by a surrounding try. Second, promises are a discipline imposed on callbacks: single settlement, automatic propagation, composability. When you read await next, you are reading syntax over that discipline.

Exercise

A third-party library accepts a callback and your telemetry shows it sometimes fires twice. What is the failure mode, and how do promises change it?

Show answer

Double invocation violates the single-call assumption and silently corrupts any state the callback mutates — a consequence of inversion of control with no settlement contract. A promise settles exactly once by construction: further resolve/reject calls are ignored, so double-settlement cannot re-run reaction logic.

Exercise

Why can't a try/catch wrapping an async call catch an error thrown inside the callback?

Show answer

The try block finishes executing long before the callback runs. The callback is invoked later as a new stack frame from the event loop, so any throw happens outside the original try's dynamic extent and becomes an uncaught exception (or a promise rejection, in promise APIs).

Exercise

You need to react to every message on a websocket. Is this a promise-shaped problem or a callback-shaped one? Why?

Show answer

Callback-shaped. A promise models one future value with single settlement; a message stream delivers many values over time. The native idioms are event callbacks (or async iteration — for await ... on an async iterator — which is the promise-world's answer to streams, covered later).

Spotted a mistake, or code that does not work?

Report an issue

Chapter: Asynchrony with Callbacks

0 / 2,000 characters

Plain text, for example: the second code sample does not compile. Do not include personal information.

You can't do that