skip to content

Promise Combinators

The built-in ways to run promises together — all, allSettled, race, any — plus the concurrency control none of them gives you. Interviewers ask which one you would pick, because the wrong choice silently loses errors or floods a downstream service.

part ofJavaScriptoverview, primer and where to startread it →
on this pageshow

explore

questions

12

What is the difference between Promise.all and Promise.allSettled in JavaScript, and what does each of them fulfil with?

level: juniorimportance: must knowfreq 78%

answer

  1. one aggregate promise, two failure policies
  2. fail-fast versus wait-for-everyone
  3. status descriptors instead of bare values
  4. first rejection reason, other results discarded
  5. ES2020 addition that never rejects

basics

~20 s

Promise.all rejects the moment any input promise rejects, and otherwise fulfils with an array of the values in input order. Promise.allSettled waits for every input and always fulfils, giving one {status, value or reason} object per entry.

solid answer

~40 s

Both take an iterable of promises and return a single aggregate promise, but they disagree on what counts as done. `Promise.all` is all-or-nothing: it fulfils with an array of values, positionally matched to the input, only if every input fulfils; if any input rejects it rejects immediately with that first rejection reason and discards the other results. `Promise.allSettled`, added in ES2020, never rejects — it waits for every input to settle either way and fulfils with an array of descriptor objects, `{ status: 'fulfilled', value }` for successes and `{ status: 'rejected', reason }` for failures. So I reach for `all` when the operation is meaningless without every piece and I want the error to propagate, and for `allSettled` when partial results are useful and I need to see which entries failed and why.

go deeper

for a junior

Be able to state the two behaviours plainly: one rejects on the first failure and gives you an array of values, the other always succeeds and gives you a status object per entry. Naming the {status, value, reason} shape from memory is expected.

for a middle

Explain the mechanics behind the difference — positional result ordering, fail-fast rejecting with a single reason, and the fact that on the happy path both settle at the same time. Be ready to write the filter-by-status code that splits successes from failures.

for a senior

Show judgment about which failure policy the caller actually needs, and point out that a fail-fast rejection leaves the sibling work running with its side effects intact. Interviewers expect you to raise the diagnostic loss when only the first reason survives.

for a principal

Own the API-contract angle: an all-or-nothing endpoint and a partial-results endpoint are different contracts, and switching to allSettled means the response must carry per-section status plus logging for the swallowed reasons. Decide it from the invariants the response must uphold, not from taste.

## The shared shape `Promise.all` and `Promise.allSettled` are both static methods on the `Promise` constructor. Each takes an **iterable** — normally an array, but any iterable works — walks it, passes every entry through `Promise.resolve` so plain values and thenables are treated uniformly, and returns one new promise that aggregates the whole batch. Neither of them *starts* anything: by the time you build the array, the functions inside it have already been called and the underlying work is already in flight. The combinator is only a way to observe when the batch is done. They differ in exactly one decision: what counts as "done", and therefore what the aggregate promise settles with. ## Promise.all — all-or-nothing `Promise.all` fulfils only when **every** input has fulfilled. Its fulfilment value is an array of the individual values, in the **order of the input iterable**, regardless of which finished first. ```js const [user, orders] = await Promise.all([fetchUser(id), fetchOrders(id)]); ``` If any input rejects, the aggregate rejects **immediately** with that single rejection reason — not an array of reasons, not a partial result. This is called fail-fast. The `await` above throws exactly as if a single call had thrown, which is why `Promise.all` composes so naturally with `try/catch`: the failure of any part is the failure of the whole. Two consequences follow that candidates often miss. First, the values of the inputs that did succeed are gone — the aggregate promise settles once, and it settled as a rejection. Second, rejecting the aggregate does not stop the other inputs: a promise is a handle on work that is already running, not a job you can call off, so the remaining requests still complete and their side effects still land. ## Promise.allSettled — a report per entry `Promise.allSettled` (ES2020) fulfils once every input has **settled**, fulfilled or rejected. It essentially never rejects; you always get an array back, again positionally matched to the input. Each element is a descriptor object: ```js const results = await Promise.allSettled([a(), b(), c()]); // [{ status: 'fulfilled', value: 1 }, // { status: 'rejected', reason: Error }, // { status: 'fulfilled', value: 3 }] ``` A fulfilled descriptor has `status` and `value`; a rejected one has `status` and `reason`. The other property is simply absent, so reading `.value` on a rejected entry gives `undefined` rather than throwing. Because the aggregate always fulfils, **you** are responsible for inspecting statuses — nothing throws on your behalf: ```js const ok = results.filter(r => r.status === 'fulfilled').map(r => r.value); const failures = results.filter(r => r.status === 'rejected').map(r => r.reason); ``` ## Timing: which one settles sooner A common assumption is that `allSettled` is "the slow one". On the happy path both settle at the same moment — when the slowest input finishes. They diverge only when something rejects: `all` can settle much earlier (the instant of the first rejection), while `allSettled` still waits for every straggler. If you need every operation to be finished before you continue — cleanup, a summary log, a transaction boundary — `allSettled` is the one that actually guarantees it. ## Choosing between them Ask whether the caller's result is meaningful without one of the pieces. A page that needs the user record *and* their permissions is all-or-nothing: use `all` and let the rejection propagate. A dashboard with five independent widgets, or a batch importer processing 200 rows, is not: use `allSettled`, render what arrived, and report the rest. ## Edge cases worth knowing - Both fulfil immediately with `[]` for an empty iterable. - Non-promise entries pass straight through: `Promise.all([1, Promise.resolve(2)])` fulfils with `[1, 2]`. - `Promise.all` rejects with the *first* reason only; later rejections are dropped. They do not surface as unhandled rejections, because `all` attached handlers to every input up front — but the diagnostic detail in them is lost unless you capture it yourself. - Neither offers cancellation, and neither retries; those are separate concerns you layer on top.

  • If two of the inputs to Promise.all reject, what happens to the second rejection?
    The aggregate promise has already settled with the first reason, so the second is simply ignored — a promise settles once. It does not become an unhandled rejection either, because `Promise.all` attached a handler to every input when it was called. The practical cost is diagnostic: you never see that second error unless you wrap each input in its own `.catch` or use `Promise.allSettled` instead.
  • Does Promise.allSettled ever reject?
    For practical purposes no — every input settling either way still fulfils the aggregate. The only realistic way to get a rejection is a malformed argument: passing a non-iterable, or an iterator whose `next` throws, rejects the returned promise with that error. That is a programmer mistake, not a task failure, so you still cannot rely on `try/catch` to reveal that one of your operations failed.
  • What does Promise.all fulfil with if you pass it an empty array?
    It fulfils right away with an empty array. `Promise.allSettled([])` behaves the same way. This matters when the batch size is computed at runtime: an empty result is not a signal that something went wrong, so code that treats `results.length === 0` as an error will misfire on the legitimate no-work case.

saying these in an interview costs you the question

  • Claims Promise.all rejects with an array of all errors
  • Says Promise.allSettled rejects when any entry fails
  • Thinks Promise.all returns results in completion order
  • Believes a rejected Promise.all cancels the remaining work
  • Assumes allSettled entries are the plain resolved values

context

open as a page

You run `await Promise.all(urls.map(url => fetch(url)))` over an array of 5,000 URLs. What actually happens, and why can Promise.all not limit how many requests are in flight?

level: juniorimportance: must knowfreq 62%

basics

~20 s

All 5,000 requests start immediately: map calls fetch for every URL before Promise.all ever runs. Promise.all only observes promises that are already in flight, so any concurrency cap must be applied where the work is started.

open as a page

In JavaScript, what does the promise returned by Promise.race(iterable) settle with, and what happens if the input that settles first is a rejection?

level: juniorimportance: must knowfreq 68%

basics

~20 s

Promise.race settles with the first input promise to settle and copies its outcome either way. If that first settled input rejects, the race rejects with the same reason, even when other inputs would have fulfilled.

open as a page

Implement `mapWithConcurrency(items, limit, worker)`, which keeps at most `limit` calls to the async `worker` running at once and resolves to an array of results in the original `items` order. How does your implementation work?

level: middleimportance: must knowfreq 55%

basics

~20 s

Start exactly limit runner loops. Each pulls the next index from a shared cursor, awaits worker for that item, and writes the value into results[index] before pulling again. Awaiting all runners gives results in input order.

open as a page

Three uploads are running under a single Promise.all and the second one rejects after 100 ms, so the await throws right away. What is happening to the other two uploads, and what problems does that create?

level: seniorimportance: must knowfreq 55%

basics

~20 s

They keep running to completion. Promise.all's fail-fast rejection only settles the aggregate promise; it has no power to stop work already in flight, so the other uploads still finish, still write their side effects, and their outcomes are silently discarded.

open as a page

Promise.allSettled never rejects. Given that, what must your code do with the array it fulfils with, and what bug appears if you treat those entries as if they were plain resolved values?

level: middleimportance: should knowfreq 52%

basics

~20 s

You must inspect each entry's status yourself: fulfilled entries carry value, rejected ones carry reason and no value at all. Treating the array as plain values yields undefined for every failure, and a try/catch around the await never fires, so failures pass silently.

open as a page

When Promise.all fulfils, in what order do the values appear in the resulting array, and what happens to non-promise entries in the iterable you pass it?

level: middleimportance: should knowfreq 58%

basics

~20 s

Promise.all fulfils with values in the order of the input iterable, never in completion order, so index 0 always holds the first input's value. Non-promise entries are passed through Promise.resolve and appear unchanged at their own positions.

open as a page

How does Promise.any decide what to settle with, and what exactly is the rejection value when every promise you passed it fails?

level: middleimportance: should knowfreq 52%

basics

~20 s

Promise.any fulfils with the value of the first input promise to fulfil, ignoring rejections along the way. Only if every input rejects does it reject, with an AggregateError whose errors property holds every reason in input order.

open as a page

To cap concurrency at 10 over 1,000 jobs you can either slice the list into sequential batches of 10 and await `Promise.all` on each batch, or run 10 workers that pull from a shared queue. What does the batching version cost you?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Each batch is a barrier: it takes as long as its slowest job while the other nine slots sit idle. With uneven durations that wastes most of the capacity, whereas a pool refills a slot the instant one frees. Batching is simpler and gives clean checkpoints.

open as a page

You add a deadline to a request with Promise.race([work(), rejectAfter(5000)]). When the timeout branch wins, what actually happens to the work promise and to the pending setTimeout, and what problems does that cause in a long-running service?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Nothing stops. The race rejects, but the work promise keeps running to completion with its result discarded, its side effects still landing, and the timer stays scheduled whenever the work wins instead. Promise combinators end the wait, not the work.

open as a page

A dashboard endpoint fans out to five independent services with Promise.all, and one flaky service takes the whole page down. How do you decide whether to keep all-or-nothing or move to Promise.allSettled, and what has to change if you switch?

level: principalimportance: should knowfreq 38%

basics

~20 s

Decide from the invariant: keep Promise.all when the response is meaningless or misleading without every part, and move to Promise.allSettled when sections are genuinely independent. Switching changes the response contract, so the payload must mark which sections are degraded and the reasons must be logged.

open as a page

A nightly job fans out tens of thousands of HTTP calls to one downstream service through a concurrency limiter. How do you decide what the limit should be, and why is a hardcoded constant like 10 usually the wrong long-term answer?

level: principalimportance: should knowfreq 33%

basics

~20 s

Derive the limit from the dependency's budget, not from your loop: target throughput times average latency gives the in-flight count you need. A per-process constant is wrong because the downstream sees every replica's limiter multiplied together, and because the right number drifts as latency and capacity change.

open as a page