Asyncintermediate
Promise Chaining Pitfalls
Common mistakes when chaining .then() calls, including forgetting to return, nested chains, and swallowed errors.
Four mistakes account for nearly every broken promise chain.
A missing return in a promise chain is like handing off a relay baton to the wrong routine — the runner keeps going with an empty hand, and nobody notices the real baton was dropped until someone reaches for it.
Key Concepts
1
1. Forgetting to return.
.then(user => { fetchOrders(user.id); }) // returns undefined
.then(orders => orders.length) // TypeError2
The inner promise starts and the chain does not wait for it. The next handler receives undefined, and the
inner rejection — if any — becomes unhandled.
undefined
3
.then(user => fetchOrders(user.id)) // concise arrow returns
.then(user => { return fetchOrders(user.id); })4
The concise arrow form avoids this entirely, which is a reason to prefer it.
5
2. Nesting instead of chaining.
fetchUser(id).then(user => {
fetchOrders(user.id).then(orders => { // callback hell,
fetchItems(orders[0]).then(items => { // with promises
...
});
});
});6
fetchUser(id)
.then(user => fetchOrders(user.id))
.then(orders => fetchItems(orders[0]))
.then(...)
.catch(handle);7
The flat version has one error handler. The nested one needs a catch at every level, and the inner
rejections do not reach the outer handler.
catch
8
3. A catch in the wrong place.
fetchUser(id)
.catch(handle) // catches only fetchUser
.then(u => fetchOrders(u.id)) // errors here are unhandledcatch
9
A catch covers what precedes it. Put it last, or add a second one where you genuinely want to recover and
continue.
catch
10
And note that a catch returns a fulfilled promise, so the chain continues after it:
catch
11
.catch(() => null) // recover with a default
.then(v => ...) // runs, with v === null12
4. Losing concurrency.
const a = await fetchA(); // sequential
const b = await fetchB();13
const [a, b] = await Promise.all([fetchA(), fetchB()]);14
If b does not need a, the first version takes the sum of the two durations for no reason.
ba
15
The subtler version is starting them concurrently and awaiting later:
16
const pa = fetchA(); // both start now
const pb = fetchB();
const a = await pa;
const b = await pb;17
Which is concurrent, and worth knowing — though Promise.all says it more clearly and handles rejection
better.
Promise.all
18
The check worth running on any chain. Does every branch return? Is there a catch at the end? Does
anything await something it does not depend on?
catch
19
What the interviewer is probing.1. "What does returning a promise inside then do?" Probing: flattening. Stalls: "Nests it."
Moves up: the chain waits for it and unwraps the value, which is why nesting thens is unnecessary.
20
2. "Why does a forgotten return break a chain?" Probing: the common bug. Stalls: "It does
not." Moves up: the next then gets undefined and runs before the inner work finishes, producing a
race that looks intermittent.
21
3. "What is the promise constructor antipattern?" Probing: recognition. Stalls: "Nothing
wrong with it." Moves up: wrapping an existing promise in new Promise, which adds a layer where
errors can be lost — return the original.
22
4. "How do you run two independent async calls efficiently?" Probing: parallelism. Stalls:
"Await them in order." Moves up: start both, then await Promise.all — awaiting sequentially
doubles the latency for no reason.