What Callback Hell Actually Is
Callback hell isn't about indentation or pyramid-shaped code. Flatten the pyramid with named functions and the exact same problems survive intact, because the real issue is what a callback does to your control, not how it looks on the page.
The last two posts built up why a single thread needs some way to manage work that doesn't finish immediately. A callback is the plainest possible answer to that: a function you hand to something else, to be run once that something else is ready.
function afterTimer() {
console.log('Cart saved');
}
setTimeout(afterTimer, 1000);Describe that as "wait 1000 milliseconds, then log a message" and you've said something true but not nearly deep enough. There's a real split happening here that description skips right past.
Now and Later
Every callback slices your program into two halves. Everything up to and including the call that registers the callback is "now." It runs immediately, in order, exactly the way you'd expect any normal code to run.
Everything inside the callback itself is "later." Later has no fixed definition. It might mean a 0-millisecond timer, a network response, a message from a Web Worker, or something else entirely. The only thing "later" guarantees is that it isn't now.
ExpandNOW: the synchronous code up to the async call. LATER: the callback, running whenever the gap actually closes.
This is why a callback also goes by another name: a continuation. You run the first half of your program now, wrap the second half inside a function, and hand that function off to run later. Whenever later actually arrives, your program picks up exactly where it left off.
The Popular Explanation of Callback Hell Is Wrong
Ask most developers what "callback hell" means and you'll get some version of: nested callbacks, indentation creeping to the right, the "pyramid of doom." Fair enough, that shape is genuinely painful to read.
But the nesting isn't the actual problem. It's just one symptom, and it's possible to remove the symptom entirely while every real issue stays exactly where it was.
Take a small checkout flow: load the cart, apply a discount, then render a receipt. Written the nested way, it looks like the pyramid everyone complains about:
loadCart(function (items) {
applyDiscount(items, function (discountedItems) {
renderReceipt(discountedItems);
});
});Now rewrite the exact same flow using named functions instead of functions stacked inline, a style sometimes called continuation-passing style:
function afterLoad(items) {
applyDiscount(items, afterDiscount);
}
function afterDiscount(discountedItems) {
renderReceipt(discountedItems);
}
loadCart(afterLoad);No pyramid. No rightward drift. Nothing here looks like the code anyone points at when they say "callback hell."
And yet it has exactly the same problems as the nested version. afterLoad still doesn't run until loadCart decides to run it. afterDiscount still doesn't run until applyDiscount decides to run it. Flattening the visual shape changed nothing about what's actually happening underneath.
That's the real proof that indentation was never the disease, only ever a visible symptom of it.
So What Is Actually Wrong?
If it isn't the shape of the code, what is it? Two real problems live underneath every callback-based program, and they don't go away no matter how the code is formatted.
One is about who is actually in charge of your program once you hand a callback to someone else's function. The other is about whether a human being can look at that code and reason through what it does, in order, the way you'd read a sentence.
Both of those problems deserve a full explanation on their own, not a rushed one squeezed in here. They're exactly what the next two posts in this chapter dig into, starting with the question of control.
The Essentials
- A callback is a continuation: the second half of your program, wrapped up and deferred until later. "Now" is everything before the async call; "later" is everything inside the callback.
- "Later" has no fixed meaning. It could be a 0ms timer, a network response, a worker message, anything, the only guarantee is that it isn't now.
- Callback hell is not the pyramid shape. Rewriting nested callbacks as named functions removes the indentation entirely.
- The named-function rewrite proves the point: every real problem survives the rewrite untouched. The shape was never the disease.
- Two genuine problems live underneath every callback, one about control, one about whether the code can actually be reasoned through, and both need their own real explanation.
Next up: the first of those two problems, what happens to your control the instant you hand a callback to code you didn't write.
Keep reading