You write .then(fn) every day. You call fetch, you chain .then onto it, and you trust the object in between to hold the pending result until the network answers. Between fetch(...) and .then(fn) something does real work — and if you're honest, you've never been asked to explain exactly what.

So let's build one from nothing. Over the next seven screens you'll hand-roll the object fetch returns. Each screen names a wall; whatever you write to get past the wall is a clause in a spec that was frozen twelve years ago — we won't pin the clause numbers on until the end.

Start where the web started. Before .then, there were callbacks. You asked for a user, you passed a function; the runtime called the function back with an (err, user) pair once the network answered. One hop reads fine. Three hops start nesting. Five hops: you're writing the same if (err) branch on every floor of the building, and the building keeps growing.

Async handle · no value yet
depth 3
Depth3
Error branches0
LOC9
Callback pyramid · livehappy path only
1
// Final result: details of the user's latest order.
2
// Caller passes `done(err, result)` as the outer callback.
3
fetchUser(userId, (err, user) => {
4
  fetchOrders(user.id, (err, orders) => {
5
    fetchDetails(orders[0].id, (err, details) => {
6
      setDetails(details); done(null, details);
7
    });
8
  });
9
});

The code above runs a real workflow: fetch the user, then fetch THAT user's orders, then fetch the details on their latest order. Each step waits for the one before it because each one's arguments come from the one before. Callbacks are the glue. Now flip the “handle errors” switch and watch the line count.

Every nested lambda needs its own if (err) return done(err) branch. You cannot “share” one branch across the nest, because each layer's err variable is a fresh parameter bound to a fresh lambda scope. The code didn't get three times longer by accident; it got three times longer by necessity. And there is no way for fetchUser to hand you its result BEFORE the value exists. The only code that can see “user” is the lambda you passed in. The answer lives in the basement.