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/awaitreads better. async/awaitis syntactic sugar over promise chaining, not a replacement.
Quick example
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 chainEach .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.
// 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/awaitis cleaner there.
Common mistakes
Mistake 1: forgetting to return inside .then()
// 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
// 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
// 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
Promise.resolve(5)
.then(x => x * 2) // returns Promise<10>, not the number 10
.then(y => console.log(y)); // logs: 10This 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: chainfetchcalls 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
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
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()
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 totalWhen 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 readyA concise answer to help you respond confidently on this topic during an interview.