What Callback Hell Actually Is

Why callback hell has almost nothing to do with nested indentation, proven by rewriting a nested callback so it isn't nested at all.

September 5, 20263 min read4 / 17

Here's the actual idea this post teaches, in one sentence: callback hell isn't caused by nesting, it's caused by not being able to trust a callback to behave and by nesting being the only way to show one step waiting on another, and both of those stay just as broken even when the code isn't nested at all.

I used to think callback hell just meant "too much indentation." Flatten the code, problem solved. That's wrong, and I can prove it: I'll remove every bit of nesting below and the exact same problem stays.

A Callback Splits Your Code Into Now and Later

Take the simplest possible callback:

JavaScript
function afterTimer() { console.log('Cart saved'); } setTimeout(afterTimer, 1000);

Most people would describe this as "wait one second, then log a message." That description misses something important.

This one line of code splits your whole program into two halves.

The first half is everything up to setTimeout(afterTimer, 1000). It runs right now.

The second half is everything inside afterTimer. It runs later, whenever the timer decides to call it.

That's why a callback is sometimes called a continuation. It's the second half of your program, packed into a function and handed off to run later.

You don't get to control exactly when "later" happens. You only know it happens somewhere further down the one thread your whole program runs on.

The Nesting Everyone Blames

Here's the classic shape everyone means when they say "callback hell": three steps, each one waiting on the last.

JavaScript
setTimeout(() => { console.log('Step 1'); setTimeout(() => { console.log('Step 2'); setTimeout(() => { console.log('Step 3'); }, 300); }, 300); }, 300);

That staircase of nested functions, three levels deep, is the "pyramid" people blame for callback hell.

Now watch what happens when the exact same three steps are rewritten with named functions instead of nesting them inline.

JavaScript
function step1() { console.log('Step 1'); setTimeout(step2, 300); } function step2() { console.log('Step 2'); setTimeout(step3, 300); } function step3() { console.log('Step 3'); } setTimeout(step1, 300);

No pyramid. No staircase. Just three plain functions, each one starting the next.

Run both versions and you get the identical output, in the identical order, on the identical delay. The second version just isn't shaped like a pyramid anymore. Nothing about what the code actually does changed, only how it looks on the page.

The Real Problem Was Never the Shape

If indentation isn't the problem, something else has to be. Two things, actually.

🤝
Trust
Once you hand a callback to something else, you're betting it gets called the right number of times, with the right value, at the right moment. Nothing about a callback enforces that bet.
🧠
How Your Brain Reads Code
Your brain plans things in order, one step after another. Nesting is the only way a plain callback can say "this can't run until that one finishes," and that mismatch is where bugs live.

Neither problem cares whether your code is nested or flat. A flat callback can still call you the wrong number of times. A flat callback can still hide a step your brain has to hunt for across three different functions. Flattening the shape never touched either one.

That's also why the callback is only the first of six tools in this series, not the only one.

The next two posts take each of those problems in turn, starting with trust: what happens when the thing you hand your callback to doesn't behave the way you assumed it would.