Skip to main content

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

javascript
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:

text
Step 1, received: 10 Step 2, multiplied: 20 Step 3, total: 25

Every .then():

  1. receives the result of the previous step,
  2. returns a new promise,
  3. passes its result further down the chain.

How it works under the hood

  1. The first promise is created and enters pending. When resolve(value) is called it becomes fulfilled.
  2. The first .then() receives value and returns a new promise, which now waits for the next result.
  3. 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:

javascript
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:

text
A: 1 B: 2 C: 6

Here .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() returnsWhat happens
A plain valueIt is wrapped in Promise.resolve(value)
A promiseThe chain waits until it settles
An error (throw)The chain moves into rejected
NothingIt passes undefined on

An example that mixes values and promises:

javascript
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); // ABCD

Errors 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().

javascript
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:

text
Caught: Failure! carrying on

.catch() can also sit in the middle of a chain, to recover from an error:

javascript
Promise.reject('Failure') .catch((err) => { console.log('Caught:', err); return 'Recovered'; }) .then((v) => console.log('Next:', v));

The output:

text
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:

javascript
Promise.resolve('OK') .finally(() => console.log('Cleaning up...')) .then(console.log);

The output:

text
Cleaning up... OK

A full chain with an error, a recovery and a closing step:

javascript
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:

text
Error caught: Oops! Result: 5 Done

An 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

PropertyDescription
Every .then()Returns a new promise
Values flow downwardsThe result of one .then() goes into the next
ErrorsFall down to the nearest .catch()
.finally()Runs in any case
The chainLets you write asynchronous code linearly, with no nesting

Common mistakes

  • Forgetting return inside .then(). Without it the next step receives undefined, 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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.