Coordinating Callbacks Is Harder Than It Looks

A dashboard that loads three stats at once, has to print them in order, and why solving that with plain callbacks means managing shared state by hand.

September 5, 20264 min read5 / 17

Here's the actual idea this post teaches, in one sentence: firing off several requests at once is easy, but making their answers show up in a fixed order, as early as possible, means writing a small piece of shared bookkeeping by hand, because a plain callback gives you no other way to do it.

I used to think the hard part of loading several things at once was just firing them off together. Firing them off is the easy part. Coordinating what happens when the answers come back, in what order, is where a plain callback starts to strain.

Concurrency stops being an idea and starts being a real problem the moment you have to coordinate it. Say a dashboard needs to load three stats: inventory, shipping, and payments. All three requests fire at the same time, since waiting for them one after another would just be slower for no reason.

Here's the twist that makes this genuinely hard: whichever one comes back first has to wait its turn. Inventory always prints first, then shipping, then payments, no matter which one the server actually answers first.

Why This Twist Actually Matters

Printing things the instant they arrive feels fast. But printing them in a random order, whatever order the network happened to answer in, feels broken, even though the data itself is still correct.

Real interfaces do this constantly. A page shouldn't freeze waiting for the slowest piece of data. But it also can't show results in a different order every time you load it. You want both: speed, and a predictable order.

The Callback-Only Way to Solve It

There's no way to know ahead of time which of the three requests finishes first. So something has to hold onto whichever answers arrive early, until it's their turn to be shown.

🧩
handleResponse is left empty. Can you fill in the gap?

Clone the repo, then run node exercise.js a few times, or open the matching .html file and check the console.

Open the starter →
JavaScript
function fakeFetchStat(name, cb) { const delay = 300 + Math.random() * 700; setTimeout(() => cb(name, `${name} data ready`), delay); } const responses = {}; const order = ['inventory', 'shipping', 'payments']; function handleResponse(name, data) { if (name in responses) return; // already seen this one, ignore a duplicate responses[name] = data; for (let i = 0; i < order.length; i++) { const key = order[i]; if (!(key in responses)) return; // hit a gap, stop here for now if (responses[key] === false) continue; // already printed this one console.log(responses[key]); responses[key] = false; // mark as printed, without deleting the key } } fakeFetchStat('inventory', handleResponse); fakeFetchStat('shipping', handleResponse); fakeFetchStat('payments', handleResponse);

handleResponse runs once for each of the three stats, in whatever order they happen to finish. Here's exactly what it does, every single time it runs:

Store whatever just came back
Save it in the shared object, keyed by name.
Check every stat, in order
Start from the first one in the required order: inventory, shipping, payments.
Stop at the first gap
The moment it hits a stat that hasn't arrived yet, stop. Nothing after it can print yet either.
Print whatever's ready
Anything that's arrived and hasn't already been printed, print it now.

Say shipping arrives first. Step 3 finds inventory missing right away and stops. Nothing prints yet, shipping's data just sits in responses, waiting.

The moment inventory finally arrives, handleResponse runs again, from scratch. This time it gets past that old gap and prints both, in the required order, in one pass.

Run this a handful of times. The arrival order changes every run, since the delay is random.

The printed order never changes. That's the entire point of the responses object. It's the one thing all three callbacks share, since none of them can tell on its own whether it's safe to print yet.

Why This Doesn't Feel Like a Win

The code above works. It's also not code you want to write three separate times in the same app.

Three independent callbacks had to share one object just to agree on what order to print in. A plain callback doesn't give you a cleaner way to do that. Every time you coordinate more than one async operation this way, you're rebuilding the same small piece of shared bookkeeping by hand, with a fresh chance to get it wrong.

That's not the nesting problem either. Notice there isn't a single nested callback anywhere in this example.

It's the other problem. Plain functions and a shared object are the only coordination tool callbacks give you, and that tool stops being trustworthy once you're juggling more than a couple of things at once.

The next post names the first of the two deeper problems hiding under all of this: what happens when you can't even trust the callback to be called the way you expect.