All topics
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)              // TypeError
2
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 unhandled
catch
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 === null
12
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.