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 14

Asynchrony with Callbacks

Prerequisites: you should be comfortable with functions, closures, and the call stack from earlier chapters. This chapter assumes you know that JavaScript runs one synchronous task at a time and that functions are values you can pass around.
Level: AdvancedLength: Mid2 min read

Read at another depth

Asynchrony with Callbacks

Prerequisites: you should be comfortable with functions, closures, and the call stack from earlier chapters. This chapter assumes you know that JavaScript runs one synchronous task at a time and that functions are values you can pass around.

Why callbacks exist

JavaScript has a single call stack. Reading a file or waiting on a network would block everything if done synchronously, so the platform gives you non-blocking APIs that accept a callback: a function the platform calls later, when the work finishes. Passing a function and returning immediately inverts control — your code no longer decides when it runs. That trade-off is the root of both the power and the pain of callback-based code.

Code example in javascript
const fs = require('fs');

fs.readFile('config.json', 'utf8', (err, data) => {
  if (err) {
    console.error('read failed:', err.message);
    return;
  }
  console.log(JSON.parse(data));
});
console.log('requested'); // logs first, not last

The error-first convention

Node's ecosystem standardized on (err, ...) as the first parameter. A callback that doesn't check err first silently swallows failures. Some APIs (browser events, Array.prototype.sort's comparator) use different signatures — always check the contract before assuming error-first.

Failure modes

  • Callback hell / rightward drift: sequencing async steps nests callbacks, hurting readability and making early returns awkward.
  • Forgotten errors: an unhandled error in a callback throws asynchronously; no surrounding try/catch can catch it, because the throw happens long after that block has exited.
  • Called twice, or never: hand-rolled APIs may invoke the callback on both success and error paths. Defensive code uses a done flag or deduplicates.
  • Zalgo: an API that calls the callback synchronously sometimes and asynchronously other times breaks invariants. Rule: always release asynchronously, even for cached values.
  • Lost this and lost try/catch: callbacks detached from their object lose binding (covered in the this chapter), and exceptions inside them escape any caller.

Parallel vs. sequential

Sequential steps nest; independent work can launch concurrently and synchronize with a counter:

Code example in javascript
function parallel(tasks, final) {
  const results = new Array(tasks.length);
  let remaining = tasks.length;
  if (remaining === 0) return final(null, results);
  tasks.forEach((task, i) => {
    task((err, value) => {
      if (err) return final(err); // first error wins; later calls ignored
      results[i] = value;
      if (--remaining === 0) final(null, results);
    });
  });
}

Note the deliberate double-call guard: after an error, final may be invoked again by another task failing. Production code (or Promise.all, in the next chapter) must handle that.

Timeline showing the main script calling an async API, continuing, finishing, then the event loop invoking the callback later.
100%
A callback completing out of order relative to the main script
Show diagram source
sequenceDiagram
    participant Main as Main script
    participant API as Async API
    participant Q as Task Queue
    Main->>API: readFile(path, cb)
    API-->>Main: returns immediately
    Main->>Main: remaining synchronous work
    API->>Q: enqueue cb(result)
    Main-->>Q: stack empties
    Q->>Main: run cb()

Mitigations before promises

Naming inner functions instead of anonymous nesting, extracting per-step functions, and control-flow libraries like async.eachSeries all predate promises. They helped, but they cannot compose return values or propagate errors implicitly — which is exactly the gap promises close in the next chapter.

Exercise

Why can't a try/catch block around an async API call catch an error thrown inside its callback?

Show answer

The callback runs later, on an empty stack, from the event loop — long after the try block has exited. Asynchronous throws can only be handled inside the callback itself (or by a mechanism like promises that carries the error forward).

Exercise

In parallel above, what bug appears if a task calls its callback twice on success?

Show answer

remaining decrements below its intended count and final fires early or twice, corrupting results ordering. Guard each task so its callback runs at most once.

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