Chaining Promises Is the Least Important Part

How returning a promise from inside .then() chains two promises together, why the value flows forward automatically, and why the flattened shape people get excited about barely matters.

September 6, 20262 min read13 / 17

Here's the actual idea this post teaches, in one sentence: chaining a promise just means returning a new promise from inside a .then() handler, which makes the next step wait for it and receive its value automatically, and the flat, non-nested shape everyone gets excited about is the least important thing about it.

The Same Pyramid, Rewritten

Three genuine dependencies, three levels of nesting, no way around it with plain callbacks:

JavaScript
getBasePrice('sofa-142', function (base) { getDiscount(base, function (discounted) { getFinalTotal(discounted, function (total) { console.log(`Final total: $${total.toFixed(2)}`); }); }); });

Rewrite each function to return a promise instead of taking a callback, and the exact same sequence looks like this:

JavaScript
function fakeAsync(value) { return new Promise((resolve) => setTimeout(() => resolve(value), 300)); } function getBasePrice(sku) { return fakeAsync(120); } function getDiscount(base) { return fakeAsync(base * 0.9); } function getFinalTotal(discounted) { return fakeAsync(discounted * 1.08); } getBasePrice('sofa-142') .then((base) => getDiscount(base)) .then((discounted) => getFinalTotal(discounted)) .then((total) => console.log(`Final total: $${total.toFixed(2)}`));

No nesting. Just a straight vertical line.

This isn't automatic magic. It comes down to one specific rule: whatever you return from inside a .then() handler becomes what the next .then() waits for.

getDiscount(base) returns a promise. Returning it from inside the first .then() tells the chain: don't move on yet, wait for this one too. The second .then() only runs once that returned promise settles.

getBasePrice resolves with 120
The first .then() receives 120 as base.
getDiscount(base) is returned
The chain pauses here until this new promise resolves.
The second .then() receives the discounted value
Whatever getDiscount resolved with becomes discounted, automatically.

That's the whole mechanism. Each step's resolved value flows straight into the next step's callback, as an argument, with nothing extra written to carry it there.

The Part That Doesn't Actually Matter Much

Here's the thing worth saying plainly: that flattened, non-nested shape is the least important thing a promise chain gives you.

It's not nothing. A vertical chain is genuinely easier to scan than three closing braces stacked on top of each other. But the pyramid was never actually the disease, remember. It was always just the most visible symptom.

What actually matters is everything from the last two posts: the value can't be changed after the fact, an error anywhere in the chain has exactly one place to land, and each step is genuinely guaranteed to run once. A chain of thunks, or a chain of plain callbacks, can look just as flat as this. Neither one gives you any of those guarantees.

It's easy to get excited the first time you see a promise chain replace a pyramid and think that's the whole win. It isn't. The shape changed. The trust is what's actually new.

There are better tools than a long .then() chain for real flow control, and this series gets to them soon. For now, the mechanism is simple: return a promise, and the chain waits for it.