learn
JavaScript

Callbacks and callback hell

Error-first callbacks, nesting, and the escapes that actually work.

A callback is a function you hand over to run later — and nesting them turns error handling into a rightward pyramid. Interviewers ask this because the pyramid is style, but the trust problem underneath (called once? called ever?) is why promises exist.

Real-life situation

A profile page loads the user, then their orders, with error handling at each step. Nested inline, the happy path drifts right and every level repeats its guard. Named functions flatten it back — and promisifying ends the nesting for good.

Seen in: Express 4 error middleware receives errors through next(err), the error-first convention at framework scale (Express 4, 2014, error handling).

Visual

Callback nesting
BrowserAPIRealtimeCacheDatabaseShareddurableephemeral

Try it

Nested profile load
javascriptTab moves to the next control
function readUser(id, done) {
setTimeout(function () {
  done(null, { id: id, name: "ada" });
}, 5);
}
function readOrders(user, done) {
setTimeout(function () {
  done(null, ["book", "pen"]);
}, 5);
}
readUser(1, function (error, user) {
if (error) {
  console.log("user failed");
  return;
}
readOrders(user, function (err, orders) {
  if (err) {
    console.log("orders failed");
    return;
  }
  console.log(user.name + ":" + orders.length);
});
});
Press Run to see the output.
Expected output
ada:2
Runner helpers

sleep(ms) waits; fakeFetch(url) returns deterministic sample data. Console code runs in a worker with a 5-second limit.

Try deleting the if (error) guard and passing a failing reader — watch the crash move somewhere unhelpful. Then extract both inline callbacks into named functions and feel the pyramid flatten.

When callbacks still win

Single-shot "when" events with no failure mode: listeners, timers, array iteration. Callbacks for events, promises for operations — and convert at the boundary with a wrapper instead of mixing both in one flow.

Error first — guess first, then reveal.

predict.js
function load(done) {
done(new Error("offline"));
}
load(function (error, data) {
if (error) {
  console.log("caught");
  return;
}
console.log(data);
});

Interview questions

What is a callback?basic

A function you hand to another function to invoke later — when a click happens, a timer fires, or data arrives. Callbacks are how JavaScript expresses "do this when…": the caller owns the WHEN, your function owns the WHAT. Event listeners, array methods and timers are all callbacks in daily clothes; recognizing the pattern matters more than the name.

Interviewers also ask

  • Name three callbacks you wrote this week.
What is callback hell and how do you escape it?intermediate

Nested callbacks indenting rightward with error handling copy-pasted at every level — unreadable and unfixable. Escapes, weakest to strongest: name the functions instead of nesting anonymous ones; return early on errors; promisify into a chain; go async/await for straight-line code. Modern answers lead with promises/await; naming functions is the zero-dependency fix interviewers still accept.

Interviewers also ask

  • Show the promise version of a nested read.
What is the inversion-of-control problem with callbacks?intermediate

You hand YOUR continuation to someone else's code and trust it: to call once, to call ever, to pass sane arguments. Promises were invented to take that trust back — a promise calls back exactly once, assimilates foreign thenables safely, and centralizes errors. Name this in interviews: "the pyramid is style; inversion of control is the real defect."

Interviewers also ask

  • How do promises fix trust?
How do Node-style error-first callbacks work?basic

The callback's first parameter is reserved for the error ((err, data) => ...): non-null means failure, null means success, data rides second. Every async step checks err first and bails early — the convention that made callback pyramids carry their error handling visibly. Promises and await internalize the same discipline as rejections and catch.

Interviewers also ask

  • Where did the error go in promise chains?
How do you convert a callback API to promises?intermediate

Wrap it: new Promise((resolve, reject) => legacyFn(arg, (err, data) => err ? reject(err) : resolve(data))). Node ships this as util.promisify for error-first APIs. Convert at the boundary once, then write everything downstream as chains or await — never mix raw callbacks and promises in one flow if a wrapper can separate them.

Interviewers also ask

  • What does util.promisify assume about the callback?
When are plain callbacks still the right choice?basic

For single-shot "when" events with no failure mode worth threading: event listeners, timers, array iteration. Callbacks stay simpler than promises where there is nothing to chain and nothing to catch. The rule: callbacks for events, promises for operations — and say which one you're looking at when the interviewer asks why.

Interviewers also ask

  • Listeners versus promises: which for a button?

Key takeaways

  • Callbacks hand control to someone else: called once, ever, with what args?
  • Error-first means (err, data) with an early bail on err.
  • Flatten with named functions; escape with promises and await.

References

Last verified: 2026-10-12.

On this page