What a Thunk Actually Is
A thunk is a function that already has everything it needs to hand you a value, sync or async, and why that removes time as a factor in how you use it.
Here's the actual idea this post teaches, in one sentence: a thunk is a function that already has everything it needs to hand you a value, so calling it never has to care whether that value was ready before you asked or only shows up later.
A Function That Already Has Its Answer
Start with the plainest possible version, no async involved at all:
function add(x, y) {
return x + y;
}
function makeAddThunk() {
return function () {
return add(10, 15);
};
}
const thunk = makeAddThunk();
console.log(thunk()); // 25
console.log(thunk()); // 25, every single timeLook closely at those two functions, because the difference between them is the entire idea this post is about.
add is a normal function. It needs x and y handed to it every time, and the answer depends on what you pass in. add(10, 15) and add(3, 4) give different answers, because you're the one choosing the numbers.
thunk takes no arguments at all. It doesn't need any, because 10 and 15 are already locked inside it, from the moment makeAddThunk() ran. Call it once, call it a hundred times, you always get 25 back. Nothing you do can change the answer, because you're not giving it anything to work with anymore.
add | thunk | |
|---|---|---|
| Takes arguments? | Yes, every time | No, none |
| Can the answer change? | Yes, depends what you pass in | No, always 25 |
| What it really is | A calculation | A value, already decided |
That's the whole trick. thunk is still just a plain function underneath, nothing new was added to JavaScript to make it. It's only being used differently: as a small box holding one value, instead of a machine you feed new numbers into. Calling it doesn't calculate anything new, it just opens the box and hands you what's already inside.
The Same Idea, When the Answer Isn't Ready Yet
Now stretch that same idea into async code. An async thunk still needs no arguments to do its job, except one: a callback to hand the value to once it exists.
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));fetchStatThunk('inventory') doesn't fetch anything yet. It hands back a small function, inventoryThunk, that already knows which stat it's responsible for. Call inventoryThunk with a callback, and eventually that callback gets the data.
Why This Actually Matters
Compare the two thunks above. The first one gives you a value back immediately. The second one makes you wait.
From the outside, using them looks almost the same. You call the thunk. You get a value out. Whether that value was sitting there already or took 700 milliseconds to show up was never something you had to think about while writing the call.
That's the real payoff. Time is the single most complicated thing to reason about in any program. A thunk removes it from the picture entirely. You use a value the same way whether it arrived instantly or arrived later.
This is also the exact idea a promise is built on. A promise is a fancier wrapper around the same thing: a container around a value that doesn't care when the value actually shows up.
Thunks are what promises look like with all the extra API stripped away.
One Tool for Making Any Thunk
Writing a custom thunk-making function for every single value gets old fast. Here's a generalized version that works for any function and any arguments:
function makeThunk(fn, ...args) {
return function (cb) {
fn(...args, cb);
};
}
const shippingThunk = makeThunk(fakeFetchStat, 'shipping');
shippingThunk((name, data) => console.log(data));makeThunk takes a function and whatever arguments it needs, minus the callback, and hands back a thunk that supplies those arguments automatically. Whatever you eventually call it with becomes the callback.
What a Thunk Doesn't Fix
Thunks don't solve the trust problem. Nothing here checks whether fetchStatThunk's callback fires exactly once, or fires at all. That's still an open question, and thunks were never trying to answer it.
What thunks do give you is smaller, but still real: one consistent shape for "a value, eventually," whether that value is ready now or later. Promises use that exact shape, and finally add the missing trust guarantees on top of it.
The Constraint That Sets Up the Next Post
One more thing worth noticing. Some thunks can be built ahead of time. Others can't.
function getBasePrice(sku, cb) {
setTimeout(() => cb(120), 300);
}
function getShippingCost(sku, cb) {
setTimeout(() => cb(15), 300);
}
function getFinalTotal(amount, cb) {
setTimeout(() => cb(amount * 1.08), 300);
}
const baseThunk = makeThunk(getBasePrice, 'sofa-142');
const shippingCostThunk = makeThunk(getShippingCost, 'sofa-142');
baseThunk((base) => {
shippingCostThunk((shipping) => {
// Only now do both numbers exist, so only now can this thunk be built.
const totalThunk = makeThunk(getFinalTotal, base + shipping);
totalThunk((total) => console.log(`Total: $${total.toFixed(2)}`));
});
});A thunk can only be built once everything it needs is already known. That's a small, easy-to-miss detail right now.
It's also exactly what separates the two different flavors of thunk this series builds on next: one that waits to do any work until you ask for the value, and one that starts working the instant it's created.
Keep reading