Callbacks Are Not Reasonable

Not reasonable, as in not able to be reasoned about. Your brain plans in a straight line, one step after another, and nested callbacks force it to keep jumping instead, which is where async bugs actually come from.

July 18, 20264 min read6 / 10

The last post covered the first real problem underneath callback hell, trust. The second problem is that callbacks are not reasonable, meaning they can't be reasoned about, not that they're unpleasant.

Your Brain Plans in a Straight Line

Nobody actually multitasks, not at the level of conscious planning. At any single instant, your highest level of thought is doing exactly one thing.

Think about planning a morning: coffee, then a shower, then the drive, then work. You laid it out step by step, even though the morning itself got derailed by something unplanned along the way, a call, a delay, an interruption. Your brain handles being interrupted by events fine, that part isn't so different from how the JavaScript engine itself responds to events. The part that matters here is that the planning itself was still linear, one thing, then the next.

Code that matches this natural, one-step-then-the-next shape is easier to hold in your head. Code that fights it is where bugs quietly creep in, right at the exact point where the way you're thinking about the problem stops matching the way the engine actually runs it.

What Your Brain Wants to Write

If you could write pseudocode with zero constraints, async code might look like this: pause on one line, then resume later, picking back up exactly where you left off.

JavaScript
placeOrder(); pause; // doesn't really exist, this is the dream, not real syntax sendConfirmationEmail(); // picks up here once resumed

That's not fake laziness, it's literally the shape your brain already plans in. The problem is that plain callbacks have no pause and no resume. The only tool they give you for "this can't happen until that finishes," a relationship with a name, temporal dependency, is nesting one call inside another.

Nesting Forces Your Brain to Jump

JavaScript
placeOrder(function (order) { sendConfirmationEmail(order, function () { console.log('Order confirmed'); }); });

Reading this top to bottom sounds simple enough for three lines. Reading it is not the same as tracing it. To actually understand what runs when, your eyes have to jump inward to the next nested function, then back out, then in again, a fresh jump at every level.

Your brain plans one step after another. Nested callbacks force a jump at every level instead, each one a fresh context to load. ExpandYour brain plans one step after another. Nested callbacks force a jump at every level instead, each one a fresh context to load.

It helps to picture reading by flashlight in a dark room. You can only see the small circle the light is on, not the whole layout at once. You move the light to the next spot, and the part you just left disappears back into the dark.

Nobody holds an entire real async flow in their head at once. You understand one small piece, move the light, understand the next piece, and by the time you're three or four callbacks deep across different files, keeping the earlier pieces in mind gets genuinely hard, not because anyone is a bad programmer, but because that's a fair description of the limit of working memory.

The Actual Goal

The fix isn't "stop using async code." It's writing async code that still reads like the straight line your brain already plans in, one step, then the next, without demanding a jump at every level to trace it.

That sounds like it shouldn't be possible: code that's genuinely asynchronous underneath, but reads sequentially on the page. It is possible, and that's exactly the gap every pattern later in this chapter exists to close.

The Essentials

  1. "Not reasonable" means not able to be reasoned about, not unpleasant to look at.
  2. Human planning is naturally linear, one step after another. Bugs tend to show up exactly where code stops matching that shape.
  3. Plain callbacks only have one tool for temporal dependency, nesting. There's no built-in pause and resume.
  4. Nesting forces your eyes to jump inward and back out at every level, instead of reading straight down the page.
  5. No developer holds an entire deeply-nested async flow in their head at once. That's a real memory limit, not a skill gap.
  6. The goal isn't removing async code. It's making it read sequentially anyway. Every pattern ahead in this chapter is an attempt at exactly that.

Two real problems are named now: trust, and reasonability. Some fixes have already been tried for these, and most of them made things worse instead of better. That's next.