When Errors Donât StopâThey Flow: Rethinking Error Propagation in JavaScript Promises
Error propagation in Promises works by converting thrown errors into rejections that skip subsequent .then() handlers and travel down the chain until caught, with the ability to either recover or rethrow, shaping the flow of asynchronous execution.
At first, errors feel like interruptions.
Something breaks.
Execution stops.
You handle it.
Thatâs the mental model many of us bring from synchronous code.
But Promises donât work like that.
In a Promise chain, errors donât stop the system.
They move through it.
A Chain With Two Paths
A Promise chain is often seen as a straight line:
then â then â thenBut in reality, it has two possible directions:
- a success path
- an error path
When everything goes well, values flow forward.
When something fails, the flow doesnât stopâit simply changes direction.
How Errors Enter the System
There are two main ways an error appears:
.then(() => {
throw new Error("fail")
})or
.then(() => Promise.reject("fail"))These may look different, but they are treated the same.
Throwing inside a .then() automatically turns into a rejected Promise.
The system catches it for you.
The Skip Effect
Once an error occurs, something subtle happens.
Every .then() after that is skipped.
Promise.resolve(1)
.then(() => {
throw "error"
})
.then(() => console.log("A"))
.then(() => console.log("B"))
.catch(err => console.log(err))Only the .catch() runs.
The rest are bypassed.
Not removed.
Just⌠ignored.
Until the error is handled.
Catching the Flow
When a .catch() appears, it intercepts the error.
.catch(err => {
console.log("handled")
})And hereâs the important part:
After .catch(), the chain continues again as normalUnless you throw again.
If you return a value, the chain is restored to the success path.
If you throw again, the error keeps moving.
Recovery Is a Choice
This means .catch() is not just for handling errors.
Itâs also a decision point.
Do you:
- recover and continue?
- or propagate further?
.catch(() => 100)This transforms an error into a value.
The chain moves forward as if nothing went wrong.
But if you do:
.catch(() => {
throw "new error"
})The error continues.
Local vs Global Handling
There are two ways to handle errors:
Inline (local)
.then(success, error)This catches errors at a specific step.
Downstream (global)
.catch(error)This catches anything that wasnât handled earlier.
They look similarâbut behave differently.
Inline handling is immediate.
.catch() is cumulative.
When Both Are Present
If you use both:
.then(null, err => {
console.log("handled here")
})
.catch(err => {
console.log("handled later")
})Only one runs.
If the error is handled early, it never reaches .catch().
Because once handled, the error is no longer an error.
It becomes a value again.
The Subtle Power of Return
One of the most overlooked details in error propagation is this:
What you return determines what happens next.
.then(null, () => 100)This doesnât just handle the error.
It replaces it.
And the next step receives 100.
Not the original failure.
This is how chains recover.
Errors Across Time
Unlike synchronous try/catch, Promise errors donât propagate instantly.
They move through the microtask queue.
They travel step by step.
Across time.
But conceptually, they behave the same:
They bubble until they are caught.
A Different Way to See It
Instead of thinking:
âAn error stops executionâ
You begin to think:
âAn error redirects the flowâ
The chain doesnât break.
It adapts.
It skips what no longer applies.
And continues when it can.
A Final Thought
Promise error propagation is not about failure.
Itâs about control.
It gives you the ability to decide:
- where to handle errors
- when to recover
- and when to let them continue
Because in asynchronous systems, errors are not interruptions.
They are part of the flow.
And understanding how they moveâ
is what allows you to shape what happens next.