How Values Flow Through Time: Rethinking Promise Chaining in JavaScript
Promise chaining works by creating a new Promise at each .then(), where the return value of one stepâwhether a value, Promise, or nothingâdetermines how the next step receives and continues the flow.
At first, Promise chaining looks like a neat trick.
You write:
Promise.resolve(1)
.then(x => x + 1)
.then(x => x + 1)
.then(console.log)And it prints:
3It feels like a sequence.
A clean, linear flow.
But underneath that simplicity is a system doing far more than just âcalling functions in order.â
A Chain Is Not a Sequence
Itâs tempting to think of .then() as:
âRun this, then run that.â
But thatâs not quite whatâs happening.
Each .then() doesnât continue the same Promise.
It creates a new one.
Every step is a transformation that produces a new state.
Promise â Promise â Promise â PromiseWhat looks like a chain is actually a series of handoffs.
The Hidden Contract
Each .then() follows a simple rule:
Whatever you return becomes the input of the next step
This is the entire mechanism.
If you return a value:
.then(x => x + 1)That value flows forward.
If you return a Promise:
.then(x => Promise.resolve(x + 1))The chain pauses.
Waits.
And then continues with the resolved result.
And if you return nothingâ
.then(x => {
x + 1
})Something subtle happens.
The Silent Break
In this case, the function returns undefined.
Not because you said soâ
but because you didnât say anything at all.
And so the chain continues with undefined.
The value didnât change.
It was simply never passed along.
This is one of the most common places where expectations and reality diverge.
Not MutationâTransformation
Another subtle point is this:
.then(x => x + 1)This does not change x.
It creates a new value.
Each step receives a value,
transforms it,
and passes a new one forward.
Nothing is mutated.
Everything is replaced.
The chain is not about modifying data.
Itâs about transforming it over time.
Flattening the Unexpected
One of the most powerful aspects of Promise chaining is how it handles nested Promises.
.then(x => Promise.resolve(x + 1))You might expect this to create a nested structure.
A Promise inside a Promise.
But it doesnât.
JavaScript unwraps it.
It waits for the inner Promise, then continues as if it were a normal value.
This is not accidental.
Itâs part of the resolution process.
Every step ensures that the chain remains flat.
Time Is Part of the System
Another detail thatâs easy to miss:
.then() does not run immediately.
It runs in the microtask queue.
This means the chain is not just about valuesâ
itâs about timing.
Each step is scheduled.
Each result is delivered asynchronously.
Even if everything looks synchronous.
Errors Flow Too
Values are not the only thing that travel through the chain.
Errors do as well.
.then(() => {
throw new Error("fail")
})This doesnât break the chain.
It redirects it.
The next .then() is skipped,
and the error moves forward until it is handled.
The chain adapts.
It doesnât stop.
A Different Way to See It
Promise chaining is often described as a way to handle async code.
But thatâs only part of the story.
Itâs more accurate to see it as:
A pipeline for transforming values across time
Each step:
- receives a value
- decides what to return
- hands control to the next Promise
And the system ensures everything stays consistentâ
whether the value is immediate, delayed, or wrapped in another Promise.
A Final Thought
At a glance, Promise chaining feels like a convenience.
A cleaner way to write asynchronous code.
But underneath, itâs a carefully designed system:
- values are passed, not shared
- results are unwrapped, not nested
- execution is scheduled, not immediate
And once you see that, the chain stops being just syntaxâ
and becomes a flow.
A flow where what you return determines what comes next.
And where a single missing return can quietly change everything.