All topics
Functionsbeginner

Callbacks and Callback Hell

Passing a function to be executed later, and why deeply nested callbacks became a maintainability problem promises and async/await later solved.

A callback is a function passed to another function to be called later. It is the oldest way JavaScript handles anything asynchronous.

A callback is like leaving your phone number with a restaurant host and going to wait at the bar — instead of standing in line asking 'is my table ready?' over and over (blocking), you get called back the moment it's ready, and can drink at the bar in the meantime (non-blocking).

Key Concepts

1
    fs.readFile('a.txt', (err, data) => {
      if (err) return handle(err);
      console.log(data);
    });
2
The Node convention: error first. The callback's first parameter is the error, and it is null on success. That convention exists because there is no try/catch that can reach across the asynchronous boundary.
nulltry/catch
3
Callback hell is what happens when they nest.
    getUser(id, (err, user) => {
      if (err) return handle(err);
      getOrders(user.id, (err, orders) => {
        if (err) return handle(err);
        getDetails(orders[0].id, (err, details) => {
          if (err) return handle(err);
          render(details);
        });
      });
    });
4
Three separate problems, which is why it is worse than ugly.Error handling repeats at every level, and missing one loses the error silently.
5
The shape drifts right, so the logic is hard to follow and hard to change.
6
Sequencing is manual. Running two of these in parallel and waiting for both means writing a counter by hand.
7
Promises fixed the nesting.
    getUser(id)
      .then(user => getOrders(user.id))
      .then(orders => getDetails(orders[0].id))
      .then(render)
      .catch(handle);            // one handler for all of it
8
async/await made it read like the synchronous version.
    try {
      const user = await getUser(id);
      const orders = await getOrders(user.id);
      render(await getDetails(orders[0].id));
    } catch (err) {
      handle(err);
    }
async/await
9
Same behaviour, and now try/catch works, and control flow — loops, conditionals, early returns — behaves normally.
try/catch
10
Callbacks have not gone away. Event handlers, array methods, setTimeout and observers are all callbacks, and they are the right shape for something that happens many times.
setTimeout
11
The distinction: a callback suits repeated events; a promise suits a single eventual result. Callback hell was what happened when promises did not exist and the second case had to use the first.
12
What the interviewer is probing.1. "What problem do promises solve that callbacks have?" Probing: beyond nesting. Stalls: "They look nicer." Moves up: errors propagate through a chain rather than needing handling at each level, and the callback is guaranteed to fire once.
13
2. "What is the error-first convention?" Probing: the Node pattern. Stalls: "An old style." Moves up: the first parameter is the error and the second the result, so every caller checks it explicitly.
14
3. "How do you flatten nested callbacks without promises?" Probing: structure. Stalls: "You cannot." Moves up: name the functions and pass references rather than nesting anonymous ones, which flattens the shape.
15
4. "What is a common double-callback bug?" Probing: the guarantee callbacks lack. Stalls: "Not possible." Moves up: calling back in an error branch without returning, so the success path runs too — promises cannot settle twice.