Promise chaining
Promise chaining is a sequence of asynchronous actions in which the result of one .then() is passed to the next one. Every .then(), .catch() and .finally() returns a new promise, and that is what lets you build a linear processing flow without "callback hell".
Theory
TL;DR
- Every
.then()returns a new promise, which is why calls compose into a chain. - A value returned from
.then()becomes the argument of the next.then(). - If
.then()returns a promise, the chain waits for that promise to settle. - An error at any step skips all following
.then()calls and goes to the nearest.catch(). - The chain continues after
.catch(), because.catch()also returns a new promise. .finally()always runs and does not change the value, unless it throws itself.
Quick example
new Promise((resolve) => {
setTimeout(() => resolve(10), 1000);
})
.then((num) => {
console.log('Step 1, received:', num);
return num * 2;
})
.then((num) => {
console.log('Step 2, multiplied:', num);
return num + 5;
})
.then((result) => {
console.log('Step 3, total:', result);
});The output:
Step 1, received: 10
Step 2, multiplied: 20
Step 3, total: 25Every .then():
- receives the result of the previous step,
- returns a new promise,
- passes its result further down the chain.
How it works under the hood
- The first promise is created and enters
pending. Whenresolve(value)is called it becomesfulfilled. - The first
.then()receivesvalueand returns a new promise, which now waits for the next result. - Every
.then()in the chain waits for the previous one to finish, even when a new promise is returned inside it.
An example with an asynchronous step:
Promise.resolve(1)
.then((n) => {
console.log('A:', n);
return new Promise((resolve) =>
setTimeout(() => resolve(n + 1), 1000)
);
})
.then((n) => {
console.log('B:', n);
return n * 3;
})
.then((n) => console.log('C:', n));The output, after one second:
A: 1
B: 2
C: 6Here .then() recognises that it was handed a new promise, waits until it settles (resolve), and only then runs the next step of the chain.
What you can return from then()
What .then() returns | What happens |
|---|---|
| A plain value | It is wrapped in Promise.resolve(value) |
| A promise | The chain waits until it settles |
An error (throw) | The chain moves into rejected |
| Nothing | It passes undefined on |
An example that mixes values and promises:
Promise.resolve('A')
.then((v) => v + 'B') // returns 'AB'
.then((v) => Promise.resolve(v + 'C')) // waits for another promise
.then((v) => v + 'D')
.then(console.log); // ABCDErrors in the chain
If an error happens in any .then() (throw or a rejection), every following .then() is skipped and control moves to the nearest .catch().
Promise.resolve(10)
.then((x) => x / 2)
.then(() => {
throw new Error('Failure!');
})
.then(() => console.log('this will not run'))
.catch((err) => console.error('Caught:', err.message))
.then(() => console.log('carrying on'));The output:
Caught: Failure!
carrying on.catch() can also sit in the middle of a chain, to recover from an error:
Promise.reject('Failure')
.catch((err) => {
console.log('Caught:', err);
return 'Recovered';
})
.then((v) => console.log('Next:', v));The output:
Caught: Failure
Next: Recovered.catch() returns a new promise too, so once the error is handled the chain carries on.
finally() in the chain
finally() runs always, whatever the outcome, and does not change the value unless it throws:
Promise.resolve('OK')
.finally(() => console.log('Cleaning up...'))
.then(console.log);The output:
Cleaning up...
OKA full chain with an error, a recovery and a closing step:
Promise.resolve(1)
.then((x) => x + 1)
.then(() => {
throw new Error('Oops!');
})
.catch((e) => {
console.log('Error caught:', e.message);
return 5;
})
.then((x) => console.log('Result:', x))
.finally(() => console.log('Done'));The output:
Error caught: Oops!
Result: 5
DoneAn analogy: a promise chain is an assembly line. Every .then() is a station where the part gets worked on; if something goes wrong, .catch() fixes the error and puts the part back on the line; at the end .finally() switches off the machines and sweeps the floor.
Summary
| Property | Description |
|---|---|
Every .then() | Returns a new promise |
| Values flow downwards | The result of one .then() goes into the next |
| Errors | Fall down to the nearest .catch() |
.finally() | Runs in any case |
| The chain | Lets you write asynchronous code linearly, with no nesting |
Common mistakes
- Forgetting
returninside.then(). Without it the next step receivesundefined, and the chain does not wait for a nested promise. - Nesting
.then()calls inside each other. That recreates the callback hell the chain was meant to remove; return the promise instead of nesting. - Placing
.catch()only at the start. It catches what happened above it, so errors from later steps slip past. - Thinking
.catch()breaks the chain. After it the promise is successful again and the following.then()calls run. - Assuming synchronous code inside
.then()runs immediately. Handlers always run as microtasks, after the current synchronous code. - Losing errors without a trailing
.catch(). A chain without one produces an unhandled rejection, and in Node.js that can terminate the process.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.