A promise chain is what you get when you attach one .then() call after another, so each step only runs once the step before it has finished. The mechanism underneath is more specific than "run these in order": each .then() returns a brand new promise, and that new promise is what the next .then() actually attaches to. That one fact explains why chains do not nest, and why one catch at the bottom can clean up after every step above it.
What changed in 2026
- Promise.withResolvers is now common in library code. It hands you the resolve and reject functions outside the executor, replacing the manual deferred-promise pattern chains used to build by hand. Confirm runtime support before relying on it in older environments.
- Linters flag nested then blocks by default. Most popular ESLint configs now warn when a then callback contains another then call, pushing teams toward flat chains or async await.
- AI coding assistants routinely flatten callback pyramids into chains or async await. The rewrite that used to take an afternoon is now a quick pass, but check the rewritten error handling by hand.
- Top-level await in modules changed how chains get introduced. You can start an awaited sequence at the top of a module without an async function wrapper, so new example code often skips explicit chains entirely.
How a then chain actually works
Every .then() call returns a new promise. If the callback returns a plain value, that new promise resolves with it immediately. If the callback returns another promise, the chain does not hand the next step a promise wrapped in a promise — it waits for the inner promise to settle first, then passes along the unwrapped result. That flattening is the entire trick behind chaining: async step follows async step while the chain still reads as one flat sequence.
getUser(id)
.then((user) => getOrders(user.id))
.then((orders) => orders.reduce((sum, o) => sum + o.total, 0))
.then((total) => console.log("Order total:", total))
.catch((error) => console.error("Chain failed:", error));
getOrders returns a promise, but the next .then() receives the resolved array, not a promise. Each step waits its turn automatically.
Where chains break
- Forgetting to return inside a then. If a callback returns nothing, the next step receives
undefined and fails somewhere downstream, often far from the real mistake.
- Placing a catch too early. A catch partway through a chain only handles errors from the steps above it — anything after it is unprotected unless you add another.
- Branching instead of chaining. Attaching two separate then calls to the same promise runs both independently once it settles, a common source of a step firing twice.
- Mixing a synchronous throw with no catch downstream. A throw inside a then callback becomes a rejection, just like an explicit reject — easy to forget since it looks like a plain error.
Chain patterns compared
| Pattern |
Readability |
Error handling |
Best for |
| Flat .then chain |
Good for short sequences |
Single catch at the end |
Simple linear pipelines |
| Nested then calls |
Poor, recreates the callback pyramid |
Scattered, easy to miss a case |
Rarely the right choice |
| async / await |
Reads top to bottom |
try/catch, familiar to most developers |
Most sequential logic |
| Promise.all (parallel) |
Good, explicit intent |
One rejection fails the whole group |
Independent steps that do not depend on each other |
Chaining versus async await
async/await is not a replacement mechanism, it is different syntax over the same promise chain. The order example above is identical in behavior written this way:
async function printOrderTotal(id) {
try {
const user = await getUser(id);
const orders = await getOrders(user.id);
const total = orders.reduce((sum, o) => sum + o.total, 0);
console.log("Order total:", total);
} catch (error) {
console.error("Chain failed:", error);
}
}
Both versions build the same sequence of promises; await just reads more like ordinary sequential code, which is why most teams default to it in 2026. Dynamic import(), the mechanism behind code splitting, also returns a promise, so the same return-value and error-handling rules apply either way. Explicit chains still earn their place for piping a value through several transforms, attaching a finally that must run either way, or working inside code that mixes in callback-era APIs like the message events from a web worker.
FAQ
Does every then in a chain need its own catch?
No. A single catch at the end of the chain handles a rejection from any step above it, because a rejection skips the remaining then callbacks and falls through to the nearest catch.
Why does logging a then call give me a promise instead of the real value?
Because .then() itself always returns a new promise, not the resolved value. Read or log the value inside the callback, or await the chain and log the awaited result.
Is a promise chain slower than async await?
No. They compile down to the same underlying mechanism; the difference is only how the code reads, not how fast it runs.
Where to go next