Active Thunks Solve the Coordination Problem

Why a lazy thunk accidentally serializes parallel requests, and how an active thunk uses one variable in closure to guarantee correct order no matter which side finishes first.

September 6, 20265 min read10 / 17

Here's the actual idea this post teaches, in one sentence: an active thunk starts its work the instant it's created instead of waiting to be called, which is the only way three requests can truly run at the same time while still printing back in a fixed, guaranteed order.

Why "Lazy" Breaks the Requirement

The last post mentioned two kinds of thunk without saying why the difference matters. Here's why: it decides whether your requests actually run at the same time or not.

Picture three thunks, one per file, each supposed to start fetching immediately. If a thunk is lazy, it doesn't start any work until the moment you call it. So if you call the first thunk, wait for its callback, then call the second thunk, you've just made three "parallel" requests run one after another instead.

An active thunk starts its work the moment it's created, before anyone has called it for a value yet. Create three active thunks in a row, and all three requests are already running by the time you finish creating the third one.

The Real Problem: You Don't Know Which Side Arrives First

Say a thunk kicks off a fetch immediately, and separately, someone calls the thunk later wanting the result. Two things need to happen, in some order:

  • The fetch finishes and produces a result.
  • Someone calls the thunk, handing it a callback that wants that result.

Neither one knows which order they'll happen in. The fetch might finish first and have to wait for a listener. Or someone might call the thunk first and have to wait for the fetch. The thunk has to handle both.

Bridging Both Orderings With Closure

🧩
getStatThunk is left empty. Can you build the bridge yourself?

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

Open the starter →

Here's the whole solution. Two variables, sitting in closure, are all it takes:

JavaScript
function fakeFetchStat(name, cb) { const delay = 300 + Math.random() * 700; setTimeout(() => cb(name, `${name} data ready`), delay); } function getStatThunk(name) { let data; // holds the result, if it arrives before anyone's listening let waitingCb; // holds the callback, if someone's listening before it arrives fakeFetchStat(name, (_, result) => { if (waitingCb) { waitingCb(result); } else { data = result; } }); return function (cb) { if (data !== undefined) { cb(data); } else { waitingCb = cb; } }; }

Walk through both orderings separately.

The fetch finishes first
waitingCb hasn't been set yet, so the result gets saved into data instead. Nothing to call yet.
Later, someone calls the thunk
data already has a value, so the callback fires immediately with it.
Or: someone calls the thunk first
data is still empty, so the callback gets saved into waitingCb instead of firing.
Later, the fetch finishes
waitingCb already holds a callback, so it fires immediately with the result.

Notice fakeFetchStat gets called immediately, at the top of getStatThunk, before the function even returns anything. That's the entire difference between active and lazy. Everything below it is just bookkeeping for whichever order things happen to arrive in.

Coordinating Three of Them, With No Shared Object

Create three of these, right away, one after another:

JavaScript
const inventoryThunk = getStatThunk('inventory'); const shippingThunk = getStatThunk('shipping'); const paymentsThunk = getStatThunk('payments'); inventoryThunk((data) => { console.log(data); shippingThunk((data) => { console.log(data); paymentsThunk((data) => { console.log(data); console.log('All stats loaded'); }); }); });

All three requests started the instant each thunk was created, before any of the nested callbacks below even ran. But the printed order is still guaranteed: inventory, then shipping, then payments, every single time, no matter which one's network delay actually finishes first.

There's no shared responses object here. No loop walking through an array checking what's arrived. Exercise 1's solution needed both of those, built and checked by hand.

Here, the guarantee comes from the nesting itself. shippingThunk is only ever called from inside inventoryThunk's callback, so by the time it runs, inventory has already printed.

Each thunk handles its own "did the answer arrive yet" question by itself, in the two variables closed over inside it. Nothing outside the thunk has to track any of that.

What This Doesn't Replace

Promises don't throw this pattern away. Promises are built on exactly this idea; active thunks are the plumbing underneath them.

What thunks still don't give you is a way to know whether a thunk's own callback fires the right number of times, or fires at all. That trust problem is still sitting there, unsolved by thunks or by anything in this post. Promises are what finally closes that gap, and that's exactly where this series goes next.