Promises Deep Dive: Chaining, Errors, all vs allSettled (Interview-Ready)

A practical guide to JavaScript Promises for interviews: states, chaining rules, error propagation, finally, and when to use Promise.all vs allSettled vs race vs any.

F

Frontend Interview Team

February 08, 2026

~15 min
Promises Deep Dive: Chaining, Errors, all vs allSettled (Interview-Ready)

Promises are the backbone of modern JS async code. Interviews focus on whether you understand:

  • chaining
  • error propagation
  • concurrency helpers (all, allSettled, race, any)

30‑second interview answer

A Promise represents a future value with states: pending → fulfilled/rejected. .then() always returns a new promise; returning a value passes it down, returning a promise “flattens” it, and throwing rejects the chain. For concurrency: use Promise.all for fail-fast, allSettled when partial success is OK, race for first-settled, and any for first-fulfilled.

Key points

  • .then returns a new promise.
  • Errors propagate until the nearest .catch.
  • finally doesn’t change the resolved value (unless it throws).
  • Prefer Promise.all to run tasks in parallel.

1) Promise states (know the words)

A Promise is always in one of these states:

  • pending
  • fulfilled (resolved)
  • rejected

Once fulfilled/rejected, it’s settled and can’t change.


2) The chaining rule that matters

.then returns a new promise

Promise.resolve(1)
  .then((x) => x + 1)
  .then((x) => console.log(x)); // 2

If you return a value → next .then gets that value.

If you return a promise → next .then waits for it.

Promise.resolve('A')
  .then(() => fetch('/api/user'))
  .then((res) => res.json())
  .then((user) => console.log(user));

3) Error propagation (the “bubble” model)

Promise.resolve()
  .then(() => {
    throw new Error('boom');
  })
  .then(() => console.log('never'))
  .catch((err) => console.log('caught:', err.message));

Output: caught: boom

A thrown error inside .then becomes a rejected promise.

Rule: one .catch can catch rejections from any previous step in the chain.


4) .finally behavior

Promise.resolve('ok')
  .finally(() => console.log('cleanup'))
  .then((v) => console.log(v));

finally doesn’t receive the value, and it doesn’t change it (unless it throws).

If finally throws or returns a rejected promise → it overrides the chain result.


5) Concurrency helpers (big interview topic)

Promise.all (fail fast)

Use when:

  • you need all results
  • and any failure should stop everything
await Promise.all([p1, p2, p3]);

If any rejects → Promise.all rejects immediately.


Promise.allSettled (never throws)

Use when:

  • you want results for all, even failures
const results = await Promise.allSettled([p1, p2]);
// [{ status: 'fulfilled', value: ... }, { status: 'rejected', reason: ... }]

Promise.race (first settled wins)

  • resolves/rejects with the first promise to settle.

Use case: timeouts.

const withTimeout = Promise.race([
  fetch('/api'),
  new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), 2000))
]);

Promise.any (first fulfilled wins)

  • resolves with the first fulfilled promise
  • rejects only if all reject (AggregateError)

Great when you have fallback endpoints.


6) Common pitfalls

  1. Forgetting to return in .then
fetch('/api')
  .then((res) => { res.json(); }) // missing return!
  .then((data) => console.log(data)); // data is undefined

Fix:

.then((res) => res.json())
  1. Sequential when you meant parallel

Bad:

const a = await p1;
const b = await p2;

Better:

const [a, b] = await Promise.all([p1, p2]);

Summary checklist

  • I can explain .then returning a new promise.
  • I can explain how errors propagate.
  • I know when to use all vs allSettled.
  • I default to Promise.all for parallel work.

Summary

  • .then returns a new promise (values pass through, promises are awaited).
  • Errors bubble down the chain to the first .catch.
  • Use all when you need everything and want fail-fast.
  • Use allSettled when partial success is acceptable.
  • Use race/any for timeouts/fallbacks.