This chapter follows Asynchronous data handling.
A callback is a function the runtime calls when async work completes—or on each event. Timers, DOM listeners, and many Node APIs were designed around that pattern before Promises were standardized.
Error-first callbacks in Node
Node-style APIs use (err, result) => {}: the first argument is an error or null, the second is the success payload.
import fs from "node:fs";
fs.readFile("config.json", "utf8", (err, text) => { if (err) { console.error(err); return; } console.log(JSON.parse(text));});Mixing sync throws with callback errors is a common bug—always handle err before using result.
Timers
setTimeout(fn, delay) schedules a macrotask after at least delay milliseconds. setInterval(fn, delay) repeats until clearInterval.
const id = setTimeout(() => console.log("later"), 0);clearTimeout(id);Zero delay does not run immediately; it queues after the current stack and microtasks drain. See The event loop, tasks, and microtasks.
setImmediate (Node) and queueMicrotask sit in different queues—prefer queueMicrotask when you need to run before the next timer.
DOM and browser events
Browser code registers callbacks on targets:
button.addEventListener("click", (event) => { console.log(event.type);});Events are push: the user agent invokes your handler when input arrives. Remove listeners when components unmount to avoid leaks.
Callback composition problems
Sequential async steps nest callbacks—“callback hell”—and make error handling repetitive (try/catch does not span async gaps). Patterns that help without new syntax:
- Named functions instead of deep inline nesting
- Small modules that take a callback as the last argument consistently
For most new application code, promises or async/await replace hand-rolled callback chains. Host APIs that remain callback-based can be wrapped with util.promisify (Node) or manual Promise constructors.
Continue with Promises in JavaScript.