JavaScript/The Event Loop, Microtasks, and Asynchronous Concurrency/Promise Combinators (all, allSettled, race, any)

Promise Combinators (all, allSettled, race, any)

Medium100% Free~25 mins#promises#promise-combinators#promise-all#promise-allsettled#promise-race#promise-any#async-concurrency#asynchronous
Key Learning Objectives
✓

Master the four ECMAScript Promise combinators: Promise.all, Promise.allSettled, Promise.race, and Promise.any.

✓

Understand fulfillment and rejection criteria, short-circuit semantics, and input vs completion ordering.

✓

Learn edge-case handling for empty iterables ([]), including the non-settling Promise.race([]) and AggregateError with Promise.any([]).

✓

Compare Promise.all fail-fast behavior with Promise.allSettled resiliency in distributed microservice architectures.

✓

Write polyfills for Promise.all and Promise.allSettled under interview conditions.

The Interview Problem

What is logged to the console when the four Promise combinators execute with a rejected and a resolved promise, and how do their fulfillment and rejection rules diverge?

1async function runCombinators() {
2 const p1 = Promise.reject('E1');
3 const p2 = Promise.resolve('OK');
4
5 const res1 = await Promise.all([p1, p2]).catch((e) => `all:${e}`);
6 const res2 = await Promise.any([p1, p2]).then((v) => `any:${v}`);
7 const res3 = await Promise.race([p1, p2]).catch((e) => `race:${e}`);
8 const res4 = await Promise.allSettled([p1, p2]).then((s) =>
9 `settled:${s.map((x) => x.status).join(',')}`
10 );
11
12 console.log([res1, res2, res3, res4].join(' '));
13}
14
15runCombinators();
Predict Console Output
Interactive Challenge

Select the option that matches what standard ECMAScript prints to the console:

all:E1 any:OK race:E1 settled:rejected,fulfilled

all:OK any:OK race:OK settled:fulfilled,fulfilled

all:E1 any:E1 race:E1 settled:rejected,rejected

all:E1 any:OK race:OK settled:rejected,fulfilled

V8 Engine Execution Trace
Step 1 of 6 (Line 2)

Two settled promises are initialized: p1 is rejected with 'E1', p2 is fulfilled with 'OK'.

Call Stack (Top = Active)
Global Execution Context
runCombinators()
Lexical Scope / Bindings
p1:Promise { <rejected>: 'E1' }
p2:Promise { <fulfilled>: 'OK' }
Console Stream
> [empty]

Deep Technical Breakdown

Comprehensive Comparison of the 4 Promise Combinators

JavaScript provides four distinct static methods on Promise for coordinating concurrent operations:

CombinatorAddedFulfills WhenRejects WhenShort-Circuits?
Promise.allES2015All promises fulfill (returns array in input order)Any single promise rejectsYes, on 1st rejection
Promise.allSettledES2020All promises settle (returns { status, value/reason }[])Never rejectsNo (waits for all)
Promise.raceES2015First promise settles (fulfills if 1st fulfills)First promise settles (rejects if 1st rejects)Yes, on 1st settlement
Promise.anyES2021First promise fulfillsAll promises reject (with AggregateError)Yes, on 1st fulfillment

The Empty Iterable ([]) Trap

Interviewers love testing empty array inputs because the four methods exhibit sharply contrasting behaviors:

  1. Promise.all([]): Immediately fulfills with [] (synchronous tick microtask).
  2. Promise.allSettled([]): Immediately fulfills with [].
  3. Promise.race([]): Remains pending forever! Since there are no promises to settle, the race has no contenders and never completes.
  4. Promise.any([]): Immediately rejects with AggregateError: All promises were rejected.

Polyfill Implementation: Promise.all

javascript
function promiseAll(iterable) {
  return new Promise((resolve, reject) => {
    const items = Array.from(iterable);
    if (items.length === 0) return resolve([]);

    const results = new Array(items.length);
    let completed = 0;

    items.forEach((item, index) => {
      // Wrap item with Promise.resolve to handle non-promise primitives
      Promise.resolve(item).then(
        (value) => {
          // Store at original index to preserve input order!
          results[index] = value;
          completed++;
          if (completed === items.length) {
            resolve(results);
          }
        },
        (err) => {
          // Fast fail on first rejection
          reject(err);
        }
      );
    });
  });
}

When to Use Which Combinator

  • Use Promise.all when operations depend on one another (e.g. fetching user data AND authorization token before rendering).
  • Use Promise.allSettled when tasks are independent and partial success is acceptable (e.g. bulk email sending or dashboard widgets where failed widgets render an error state without breaking the page).
  • Use Promise.race for timeouts (e.g. Promise.race([fetchData(), timeout(5000)])).
  • Use Promise.any for redundant mirrors or CDNs (fetch from 3 CDNs simultaneously, use whichever responds first successfully).
Common Traps & Mistakes

Assuming `Promise.all` results array is sorted in the order promises finish. The results array strictly preserves input order, regardless of which network request completed first.

Using `Promise.race([])` without recognizing that it creates an un-settled pending promise that never resolves or rejects.

Forgetting to wrap items in `Promise.resolve(item)` in combinator polyfills, causing non-promise values (numbers, strings) to throw errors.

Expecting `Promise.any` to reject when the first promise fails. It ignores rejections until every single promise has failed.

FAANG Follow-Up Probes
Probe #1

How would you implement a concurrency limiter (p-limit) that executes at most N promises concurrently using Promise combinators?

Probe #2

Why does Promise.race([p1, timeout]) fail to cancel the underlying HTTP request in p1, and how does AbortController solve this?

Probe #3

What is the structure of AggregateError thrown by Promise.any, and how do you access individual rejection reasons (`err.errors`)?