What A Thunk Actually Is
A thunk is a function that already has everything it needs to hand you a value back. It sounds trivial until you realize it's the exact idea a Promise is built on: a container that makes time itself stop mattering.
The last four posts laid out two real problems with plain callbacks, trust and reasonability, and two attempted fixes that solved neither. Before this chapter reaches for the pattern that actually addresses both, there's a smaller, stranger idea worth understanding first: the thunk.
A Thunk Is a Value You Haven't Unwrapped Yet
Start with the synchronous version, since it's the easiest to see. A thunk is a function that already has everything it needs to hand you a value back. No arguments, just call it.
function add(x, y) {
return x + y;
}
function makeAddThunk() {
return function () {
return add(10, 15);
};
}
const thunk = makeAddThunk();
thunk(); // 25
thunk(); // 25, every single timethunk isn't the number 25. It's a closure holding onto 10 and 15, waiting to be called. Call it as many times as you want, in as many places as you want, and it always hands back the same answer.
That's the entire trick. The function itself becomes a container wrapped around some state, a token you can pass around your whole program. Whenever you actually need the value, you call the token, and the value comes out.
Async Thunks Remove Time From the Equation
An asynchronous thunk keeps that same idea, with one twist: instead of returning a value directly, it takes a callback to hand the value to, whenever that value actually becomes available.
Reusing the dashboard's stats fetcher from the last few posts:
function fakeFetchStat(name, cb) {
const delay = 300 + Math.random() * 700;
setTimeout(() => cb(name, `${name} data ready`), delay);
}
function fetchStatThunk(name) {
return function (cb) {
fakeFetchStat(name, (_, data) => cb(data));
};
}
const inventoryThunk = fetchStatThunk('inventory');
inventoryThunk((data) => console.log(data));inventoryThunk is exactly the same kind of token as before. Call it with a callback, and the callback gets the value, whenever it's actually ready.
ExpandA thunk hides whether its value was ready instantly or arrived later. Either way, the caller writes the exact same line to get it.
Here's the part that actually matters: the code calling inventoryThunk has no idea, and no need to know, whether that value came back instantly or took a while. It might already be cached and ready, maybe the thunk memoized a result it already fetched once before. It might need a slow network round trip. The calling code is identical either way.
That's time, factored out of the equation. Understanding when things happen and how state changes over time is the single hardest part of reasoning about a program. A thunk doesn't remove that complexity from existence, it just wraps it up once, in one place, so nowhere else in the program has to think about it again.
A Generalized Way to Make Them
Writing a bespoke thunk for every function gets repetitive. A small utility can build one from any function and its arguments, reserving the callback slot for whenever it actually gets called:
function makeThunk(fn, ...args) {
return function (cb) {
fn(...args, cb);
};
}
const inventoryThunk = makeThunk(fakeFetchStat, 'inventory');This is, conceptually, a Promise constructor with the fancy parts stripped away. A Promise is really just a more sophisticated, more capable version of exactly this: a time-independent wrapper around a value.
What a Thunk Doesn't Fix
It's tempting to assume this also solves inversion of control, the trust problem from a few posts back. It doesn't. A thunk's callback is still a plain callback underneath, still subject to being called too many times, too few times, or not at all. That specific problem is still waiting for an answer, and Promises are what actually deliver it.
What a thunk does give you is real: a working model for a value that doesn't care when it arrives. That idea, not the syntax around it, is the actual foundation the rest of this chapter builds on.
The Essentials
- A synchronous thunk is a closure that already has everything it needs, call it with no arguments, get the value back.
- An asynchronous thunk takes a callback instead of returning directly, handing the value over whenever it's actually ready.
- The caller can't tell, and doesn't need to tell, whether a thunk's value was instant or delayed. The call site looks identical either way.
- This is what "factoring time out of state" actually means. The hardest part of async reasoning gets solved once, inside the thunk, instead of everywhere it's used.
- A thunk is conceptually a Promise without the API, a plain, literal preview of what a Promise actually is underneath.
- Thunks don't fix inversion of control. The callback inside a thunk still carries every trust problem a plain callback does.
Time to put this to use: the next post rebuilds the three-stat dashboard exercise with thunks instead of shared state, and runs straight into a question that decides whether it actually works, lazy or active.
Keep reading