Ch. 1 · JavaScript

Promise.all vs allSettled vs race vs any: JavaScript Interview Guide

How Promise.all, allSettled, race and any settle, what each does on rejection or empty input, and how to pick the right one for real requests.

~7 min readintermediateupdated Oct 6, 2026

“You need to load a user, their orders and their recommendations. Which Promise method do you use, and what happens if recommendations fail?” Interviewers love the four Promise combinators because each one encodes a different failure policy, and choosing between them forces you to think about what the product should do when part of the work goes wrong. Candidates who answer “Promise.all” for everything usually get a follow-up showing the whole page breaking because one optional widget timed out.

The question also checks edge cases that rarely come up until production: empty arrays, non-promise values in the input, and the fact that a rejected combinator does not stop the other operations.

Before you start

You should understand promise states (pending, fulfilled, rejected), async/await, and try/catch around await. Knowing that an unhandled rejection is reported by the runtime will help with the “losing promises” discussion.

The short answer

Promise.all fulfils with an array of all results in input order, or rejects as soon as any input rejects. Promise.allSettled always fulfils, with an array of { status, value } or { status, reason } objects, once every input has settled. Promise.race settles the same way as whichever input settles first, success or failure. Promise.any fulfils with the first success and rejects with an AggregateError only if every input rejects. None of them cancel the remaining work.

How it works

All four accept any iterable, wrap each element with Promise.resolve (so plain values are allowed), and attach handlers to every element immediately. They differ only in when they settle and with what.

const delay = (ms, value, fail = false) =>
  new Promise((resolve, reject) => setTimeout(() => (fail ? reject(new Error(value)) : resolve(value)), ms));

const inputs = () => [delay(30, 'slow'), delay(10, 'fast-fail', true), delay(20, 'medium')];

await Promise.all(inputs()).catch((e) => console.log('all rejected:', e.message));
console.log('allSettled:', (await Promise.allSettled(inputs())).map((r) => r.status));
await Promise.race(inputs()).catch((e) => console.log('race rejected:', e.message));
console.log('any:', await Promise.any(inputs()));
// all rejected: fast-fail
// allSettled: [ 'fulfilled', 'rejected', 'fulfilled' ]
// race rejected: fast-fail
// any: medium
JavaScript

Here fast-fail rejects first at 10 ms. all and race both reject with it; any ignores it and waits for the first success, medium at 20 ms; allSettled reports every outcome after 30 ms.

Empty input is a classic edge case, and each method behaves differently:

console.log(await Promise.all([])); // []
console.log(await Promise.allSettled([])); // []
await Promise.any([]).catch((e) => console.log(e.constructor.name, e.errors)); // AggregateError []
const pending = Promise.race([]);
const timeout = new Promise((r) => setTimeout(() => r('race([]) still pending'), 10));
console.log(await Promise.race([pending, timeout])); // race([]) still pending
JavaScript

Promise.race([]) never settles, because there is nothing to race. Code that builds the input list dynamically can hang forever if that list turns out empty.

Step-by-step walkthrough

Step 1: Classify each operation

For each async call, decide whether it is required or optional, and whether you need all results or just one. Required and all-or-nothing points to all. Independent tasks where partial success is useful points to allSettled. “Whichever responds first” with fallbacks points to any. “First thing to happen, including failure,” like a timeout, points to race.

Step 2: Load required data with Promise.all

When the page cannot render without every piece, failing fast is correct: no reason to wait for the others once one essential call has failed.

const api = {
  user: async () => ({ name: 'Ada' }),
  orders: async () => [{ id: 1 }, { id: 2 }],
};
const [user, orders] = await Promise.all([api.user(), api.orders()]);
console.log(user.name, orders.length); // Ada 2
JavaScript

Destructuring keeps the code readable, and order is guaranteed to match the input, not completion order.

Step 3: Collect partial results with allSettled

For a dashboard where each widget is independent, render what succeeded and show an error state for what failed.

const widgets = {
  weather: async () => '18°C',
  news: async () => { throw new Error('news API 503'); },
  stocks: async () => 'up 2%',
};
const names = Object.keys(widgets);
const results = await Promise.allSettled(names.map((n) => widgets[n]()));
const view = Object.fromEntries(
  results.map((r, i) => [names[i], r.status === 'fulfilled' ? r.value : `error: ${r.reason.message}`]),
);
console.log(view);
// { weather: '18°C', news: 'error: news API 503', stocks: 'up 2%' }
JavaScript

Step 4: Use any for redundancy and race for deadlines

any suits mirrored sources: take whichever CDN or replica answers successfully first. race suits deadlines, because a timeout is just a promise that rejects after a delay.

const delay = (ms, v, fail) => new Promise((res, rej) => setTimeout(() => (fail ? rej(new Error(v)) : res(v)), ms));

const mirror = await Promise.any([delay(5, 'mirror-a down', true), delay(15, 'mirror-b'), delay(25, 'mirror-c')]);
console.log(mirror); // mirror-b

const withTimeout = (p, ms) => Promise.race([p, delay(ms, `timed out after ${ms}ms`, true)]);
await withTimeout(delay(50, 'slow report'), 20).catch((e) => console.log(e.message));
// timed out after 20ms
JavaScript

When every any input fails, inspect error.errors on the AggregateError to see each individual reason.

Worked scenario

A product page used one Promise.all for the product, its price and a “customers also bought” carousel. When the recommendation service had an outage, every product page showed an error, even though the essential data loaded fine.

const product = async () => ({ title: 'Lamp' });
const price = async () => 25;
const recommendations = async () => { throw new Error('recs timeout'); };

async function pageBroken() {
  try {
    const [p, cost, recs] = await Promise.all([product(), price(), recommendations()]);
    return { ...p, cost, recs };
  } catch (e) {
    return { error: e.message };
  }
}
console.log(await pageBroken()); // { error: 'recs timeout' }
JavaScript

The fix separates required from optional work. Required calls stay in Promise.all so a missing price still fails the page. The optional call starts at the same time but has its failure turned into an empty fallback, so it can never reject the group.

const product = async () => ({ title: 'Lamp' });
const price = async () => 25;
const recommendations = async () => { throw new Error('recs timeout'); };

async function page() {
  const recsPromise = recommendations().catch((e) => {
    console.log('recs degraded:', e.message);
    return [];
  });
  const [p, cost, recs] = await Promise.all([product(), price(), recsPromise]);
  return { ...p, cost, recs };
}
console.log(await page());
// recs degraded: recs timeout
// { title: 'Lamp', cost: 25, recs: [] }
JavaScript

Attaching .catch to the optional promise before handing it to all is a compact way to mix policies in one call.

Common mistake

  • “Promise.all stops the other requests when one fails.” It stops waiting; the requests carry on. Pass an AbortSignal if they must actually stop.
  • “race returns the first success.” It returns the first settlement, which may be a rejection. any returns the first success.
  • “allSettled throws if something fails.” It never rejects; you must check each status.
  • Racing a dynamically built list that may be empty. race([]) hangs forever; any([]) rejects immediately.
  • Leaking a timer in a timeout race. The timeout promise’s setTimeout keeps running after the real work wins. Clear it in finally, or use AbortSignal.timeout with APIs that accept a signal.
  • Ignoring later rejections. After all rejects, other inputs that reject later are already handled internally, so they do not cause unhandled rejection warnings, but their errors are silently dropped. Log them if they matter.

Verify the behavior

This test pins down the settlement rules, including the rejection type of any.

import test from 'node:test';
import assert from 'node:assert/strict';

const ok = (v) => Promise.resolve(v);
const bad = (m) => Promise.reject(new Error(m));

test('all rejects with the first failure', async () => {
  await assert.rejects(Promise.all([ok(1), bad('x')]), { message: 'x' });
});

test('allSettled reports every outcome', async () => {
  const r = await Promise.allSettled([ok(1), bad('x')]);
  assert.deepEqual(r.map((s) => s.status), ['fulfilled', 'rejected']);
});

test('any rejects with AggregateError only when all fail', async () => {
  assert.equal(await Promise.any([bad('a'), ok(2)]), 2);
  await assert.rejects(Promise.any([bad('a'), bad('b')]), (e) => e instanceof AggregateError && e.errors.length === 2);
});
JavaScript

Run node --test combinators.test.mjs; all three tests pass.

Follow-up questions

Can you implement Promise.all yourself? Create a new promise, keep a results array and a counter, attach then to each Promise.resolve(item), store each value at its index, resolve when the counter reaches the length, reject on the first error, and resolve immediately for empty input.

Do the combinators run promises in parallel? They do not run anything; the operations started when you created the promises. The combinator only observes them.

How do you limit concurrency? Combinators do not; use a small worker pool that starts the next task as one finishes.

Why does allSettled exist if you can .catch each promise? It standardises the result shape and makes the intent explicit, so readers know partial failure is expected.

Interview exercise

Write fetchFirstAvailable(urls, fetcher, ms) that returns the first successful response from several mirrors, rejects with a clear message if all mirrors fail, and rejects if nothing succeeds within ms milliseconds. Do not leave a timer running after it settles.

Answer and reasoning

async function fetchFirstAvailable(urls, fetcher, ms) {
  let timer;
  const deadline = new Promise((_, reject) => {
    timer = setTimeout(() => reject(new Error(`no mirror within ${ms}ms`)), ms);
  });
  try {
    const first = Promise.any(urls.map(fetcher)).catch((e) => {
      throw new Error(`all ${e.errors.length} mirrors failed`);
    });
    return await Promise.race([first, deadline]);
  } finally {
    clearTimeout(timer);
  }
}
const fake = (u) => new Promise((res, rej) => setTimeout(() => (u.includes('bad') ? rej(new Error(u)) : res(`ok from ${u}`)), u.includes('slow') ? 50 : 5));
console.log(await fetchFirstAvailable(['bad-1', 'm2'], fake, 30)); // ok from m2
await fetchFirstAvailable(['bad-1', 'bad-2'], fake, 30).catch((e) => console.log(e.message)); // all 2 mirrors failed
await fetchFirstAvailable(['slow-1'], fake, 20).catch((e) => console.log(e.message)); // no mirror within 20ms
JavaScript

Promise.any expresses “first success wins” and gives an AggregateError when every mirror fails, which is translated into a readable message. Promise.race against a deadline adds the time limit, because the first of “a success” and “the deadline” should win. The finally clears the timer whichever way it settles, so the process is not kept alive by a dangling timeout. A polished answer adds that the losing requests keep running, and suggests passing an AbortController signal to fetcher and aborting it in finally.

Continue learning

Practise further with the JavaScript interview questions and the JavaScript MCQs. Related notes: Promise.race timeouts, async/await in loops and unhandled promise rejections. Official reference: Promise on MDN.

More in JavaScript

read ✓JavaScript · hard

JavaScript Timeouts with Promise.race

Why Promise.race times out the caller but not the work, how to clear the timer and abort the request, and when AbortSignal.timeout is enough.

~7 min readread →
read ✓JavaScript · hard

JavaScript Async Iteration and Backpressure

Consume async generators with for await, propagate errors and pace a producer to a slow consumer without buffering the whole stream.

~3 min readread →
esc