Skip to main content

What is promise chaining in JavaScript

Promise chaining is a pattern where each .then() returns a new Promise, so the next handler in the chain receives the previous one's result as input.

Theory

TL;DR

  • Think of it like dominoes: knock over the first, each one triggers the next automatically.
  • Each .then() creates a fresh Promise resolved with whatever your handler returns.
  • Errors bubble down the chain to the nearest .catch() - you need just one for the whole flow.
  • Use chaining for linear async steps (fetch, parse, process). For loops or conditional branches, async/await reads better.
  • async/await is syntactic sugar over promise chaining, not a replacement.

Quick example

javascript
fetch('https://jsonplaceholder.typicode.com/users/1') .then(response => response.json()) // returns Promise resolved with user object .then(user => `Hello, ${user.name}!`) // returns Promise resolved with string .then(greeting => console.log(greeting)) // logs: "Hello, Leanne Graham!" .catch(err => console.error(err)); // catches any error in the chain

Each .then() passes its return value to the next handler. If any step fails, execution skips all remaining .then() calls and jumps straight to .catch().

How the chain works internally

When you call .then(handler), JavaScript creates a new Promise and schedules handler on the microtask queue. When the previous Promise settles, the engine dequeues the job, runs your handler, and resolves the new Promise with whatever the handler returns.

If the handler returns a plain value, it gets wrapped in a resolved Promise automatically. If it returns another Promise, the chain waits for that inner Promise to settle before continuing. If it throws, the new Promise rejects.

All of this runs in the microtask queue, which the event loop drains before any setTimeout or setInterval callbacks run. A 5-step chain with sync returns completes entirely before any timer fires.

Key difference from nested callbacks

Without chaining, you nest. That is the core problem.

javascript
// nested - callback hell fetch('/user').then(response => { response.json().then(user => { fetch(`/posts/${user.id}`).then(res => { res.json().then(posts => console.log(posts)); }); }); }); // chained - flat and readable fetch('/user') .then(res => res.json()) .then(user => fetch(`/posts/${user.id}`)) .then(res => res.json()) .then(posts => console.log(posts)) .catch(err => console.error(err));

Same logic, completely different readability. The chained version also covers all errors with a single .catch().

When to use

  • 2-4 linear async steps (fetch, parse, transform, respond) - chain .then().
  • One error handler for the whole flow - single .catch() at the end.
  • Mix of sync and async returns in one flow - chaining handles both automatically.
  • Parallel operations - use Promise.all() instead.
  • Conditional branches or loops - async/await is cleaner there.

Common mistakes

Mistake 1: forgetting to return inside .then()

javascript
// wrong - returns undefined, value is lost fetch('/user') .then(res => res.json()) .then(user => { user.name; }) // no return .then(name => console.log(name)); // logs: undefined // correct fetch('/user') .then(res => res.json()) .then(user => user.name) // implicit return in arrow function .then(name => console.log(name)); // logs: "Leanne Graham"

Mistake 2: mid-chain .catch() that swallows errors

javascript
// wrong - first .catch() resolves the chain with undefined fetch('/user') .catch(err => console.log('error')) // catches but does not rethrow .then(data => console.log(data)); // runs even on failure, logs: undefined // correct - rethrow if you need the error to propagate fetch('/user') .catch(err => { console.log(err); throw err; }) .then(data => console.log(data));

Mistake 3: nesting .then() instead of chaining

javascript
// wrong - nested, breaks the flat structure fetch('/user').then(res => { return res.json().then(user => fetch(`/posts/${user.id}`)); }); // correct - return the inner promise and let the chain continue fetch('/user') .then(res => res.json()) .then(user => fetch(`/posts/${user.id}`)) .then(res => res.json());

Mistake 4: not knowing that .then() auto-wraps sync values

javascript
Promise.resolve(5) .then(x => x * 2) // returns Promise<10>, not the number 10 .then(y => console.log(y)); // logs: 10

This is correct behavior, not a bug. But it matters for timing: code outside the chain does not see 10 synchronously, even though the math itself is instant.

Real-world usage

  • Express.js: route handlers chain fetchUser().then(validate).then(respond).catch(errorHandler).
  • React useEffect: chain fetch calls to load a user and then their posts in order.
  • Axios interceptors: request, transform, and response as a built-in chain.
  • Node.js fs.promises: readFile().then(parse).then(writeFile).
  • The mid-chain .catch() mistake ships to production quietly. It only surfaces when the first request actually fails in the real environment.

Follow-up questions

Q: What happens if a .then() handler throws an error?
A: The Promise returned by that .then() rejects with the thrown error. Execution skips all remaining .then() handlers and jumps to the next .catch() in the chain.

Q: Can you have multiple .catch() calls in one chain?
A: Yes. But the first .catch() consumes the rejection and resolves the chain with its return value, unless you explicitly rethrow with throw. Later .catch() calls do not trigger without that rethrow.

Q: What is the difference between returning a plain value vs a Promise inside .then()?
A: Both work from the outside. A plain value gets wrapped in a resolved Promise automatically. A returned Promise makes the chain wait for it to settle. Either way, the next .then() receives the resolved value.

Q: How does promise chaining relate to the event loop?
A: Each .then() schedules a microtask. The event loop processes all microtasks before moving to macrotasks like setTimeout. A 5-step chain with sync returns completes entirely before any timer callback runs.

Q: When would you prefer async/await over chaining?
A: Any time you have conditional logic, loops over async operations, or need try/catch style error handling. Chains get hard to read when you add if statements inside .then(). For a simple linear sequence, chaining works fine and is sometimes shorter.

Q: (Senior) What does V8 do when a .then() handler returns another Promise?
A: V8 does not pass the returned Promise as the resolved value directly. It runs the Promise resolution procedure: calls .then() on the inner Promise and connects its settlement to the outer one. The chain pauses and inherits the inner Promise's resolved value, not the Promise object itself.

Examples

Basic: fetch a user by ID

javascript
fetch('https://jsonplaceholder.typicode.com/users/1') .then(response => { if (!response.ok) throw new Error('Not found'); return response.json(); // parse JSON, returns Promise }) .then(user => { console.log(user.name); // logs: "Leanne Graham" return user.id; }) .then(id => console.log(`User ID: ${id}`)) // logs: "User ID: 1" .catch(err => console.error('Failed:', err.message));

Each step passes a value forward. If response.ok is false, the thrown error skips the remaining handlers and lands in .catch().

Intermediate: Express route with chained DB calls

javascript
app.get('/user/:id', (req, res) => { fetchUser(req.params.id) .then(user => fetchUserPosts(user.id)) // fetch related data .then(posts => res.json(posts)) // respond with posts array .catch(err => res.status(500).json({ error: err.message })); });

One .catch() covers both DB calls. If fetchUser fails, fetchUserPosts never runs, and the error response fires immediately.

Advanced: returning a Promise from inside .then()

javascript
function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } delay(100) .then(() => { return delay(200).then(() => 'done'); // chain pauses for the inner promise }) .then(result => console.log(result)); // logs: "done" after ~300ms total

When you return a Promise from .then(), the outer chain does not continue until that Promise resolves. The result is the inner Promise's value, not the Promise object itself. This is the same mechanism that makes chained fetch calls work correctly.

Short Answer

Interview ready
Premium

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

Finished reading?