Coordinating Concurrent Callbacks

A hands-on challenge: fetch three things at once, render each one the instant it's ready, but never let a later item print before an earlier one. Plain callbacks can do it, but only by hand-rolling shared state that gets uglier with every extra request.

July 18, 20265 min read4 / 10Exercise

The last post promised two real problems hiding underneath every callback. Before unpacking either one, it's worth feeling the actual pain firsthand, and this exercise is the one this whole chapter keeps coming back to, the same problem, solved again with each new pattern as the chapter goes on.

The Challenge

A dashboard needs three stats: inventory, shipping, and payments. Each one comes from its own fake, randomly-delayed request, so you never know which one will actually finish first. Firing all three at once instead of one after another is exactly the concurrency this chapter already defined, three things making progress in the same timeframe, not the same instant.

The rules:

  • Fire all three requests at the same time, never wait for one before starting the next.
  • The moment any result is ready, render it as soon as possible. Don't hold a ready result back just because you're being tidy.
  • But never render out of order. If payments comes back first and inventory is still pending, payments has to wait. The instant inventory finally arrives, render both of them together, in order.
  • Never render the same result twice.
  • Once all three have printed, log one final "all stats loaded" line.

Rendering ASAP instead of waiting for all three isn't just a nice-to-have. A dashboard that shows nothing until every request finishes feels slower to the person watching it, even when the total load time is identical. Progressive rendering is a real, measurable improvement in how fast a page feels, not just how fast it actually is.

JavaScript ยท Live Editor
Loading editor...

Try it before reading on. Run it a few times, the random delay means the arrival order changes every run, and that's the entire point.

Why This Is Harder Than It Looks

Requesting all three at once is the easy part, one line each. The actual problem is coordinating the responses, and with plain callbacks, that means somewhere, some piece of shared state has to remember what's arrived and what hasn't.

There is no way around this. Three independent callbacks have to share state to coordinate with each other. Whether that's a global variable, a closure variable, or something passed around some other way, one of those callbacks might get its result while the others are still pending, and it needs somewhere to put that result down until it's safe to use.

Building the Solution

Start with an object to hold whatever has arrived, and an array describing the order results are supposed to print in:

JavaScript
const responses = {}; const order = ['inventory', 'shipping', 'payments'];

Every time a request finishes, the same handler runs, no matter which one it was:

JavaScript
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 } const allPrinted = order.every((key) => responses[key] === false); if (allPrinted) console.log('All stats loaded'); }

The loop is where the actual coordination happens. Every single time any result comes in, the handler walks the list from the top, in the required order, and prints whatever is ready that hasn't already been printed. The instant it hits something still missing, it stops, since nothing after that gap is safe to show yet.

The completion check only matters once the loop finishes without an early stop, which only happens once all three keys have made it past the gap check. Since handleResponse only ever runs once per stat here, that check is only ever true on the call that finishes last, whichever one that turns out to be.

Wire it up by firing all three requests at once, all sharing the same handler:

JavaScript
fakeFetchStat('inventory', handleResponse); fakeFetchStat('shipping', handleResponse); fakeFetchStat('payments', handleResponse);

Run it enough times and you'll see every possible arrival order: inventory first, payments first, shipping first. The printed order never changes. Inventory, then shipping, then payments, every single time, regardless of which one the network actually finished first.

Three requests fired at the same instant, arriving in a random order, but printing in a fixed order regardless. ExpandThree requests fired at the same instant, arriving in a random order, but printing in a fixed order regardless.

Nobody Would Call This Elegant

This works, and it's not an unusual amount of code for what it's doing. It's also not something you'd want to write three more times for three more sets of concurrent requests. Every new coordination problem means hand-rolling this same shape of state and loop again, from scratch, and getting the edge cases right by hand every time.

That's the real motivation for wanting a better pattern, not that this solution is broken, it works, but that plain callbacks give you no reusable tool for this shape of problem. Every project ends up reinventing it.

The Essentials

  1. Requesting things concurrently is easy. Coordinating the responses is the actual work.
  2. Rendering results as they arrive isn't just nicer, it makes the page feel faster, even when the total load time never changes.
  3. Independent callbacks that need to agree on ordering must share state. There's no way around a shared object, closure variable, or equivalent, callbacks alone have no other mechanism for this.
  4. A "render ASAP, but only in order" requirement needs a scan-and-stop loop: walk the required order, print what's ready, stop at the first gap, and check afterward whether everything is now done.
  5. The randomized delay is the whole point. If the arrival order were fixed, the coordination problem wouldn't exist, real network requests never guarantee arrival order either.
  6. This solution works but doesn't scale gracefully. Every new concurrent-request problem means writing this shape of code again by hand.

This exercise gets revisited with a better pattern later in this chapter. First, the two real problems this chapter promised: what happens to your control the moment you hand a callback to code you didn't write.