---
title: 'Promise Combinators'
source: 'https://academia.sh/en/courses/asynchronous-javascript/promise-combinators'
course: 'Asynchronous JavaScript and the Runtime'
language: en
updated: '2026-08-17T18:09:44+00:00'
license: 'CC BY-SA 4.0'
---

# Promise Combinators

Four combinators that gather multiple promises into a single promise; their differing behavior in the face of errors; the distinction between when work starts and when it is awaited.

The previous lesson turned a single promise into a value. What is a value can be put
in an array; what is put in an array can be handled together. The "wait for all"
function hand-written with a counter and a flag in the callbacks topic can now be
written with an operator the language provides.

This lesson introduces four combinators and shows that the real difference between
them is their behavior **in the face of errors**. The measurement stream turns here
into an aggregator that reads three stations together.

## When Work Starts

Reading the combinators correctly requires clearing up a misunderstanding first:
**combinators do not start work.** The work has already started once the promises are
created; the combinator only organizes the **waiting**.

In the examples below, the measurement source also prints the moment the request
starts. The table used throughout the course also includes the faulty `D4` station.

```js
const SOURCE = {
  A1: { delay: 30, value: 21.4 },
  B2: { delay: 10, value: 19.8 },
  C3: { delay: 20, value: 23.1 },
  D4: { delay: 15, error: "sensor fault" },
};

function requestMeasurement(station) {
  console.log("request started:", station);
  return new Promise((resolve, reject) => {
    const record = SOURCE[station];
    setTimeout(() => {
      if (record.error) reject(new Error(`${station}: ${record.error}`));
      else resolve({ station, value: record.value });
    }, record.delay);
  });
}

const measurements = await Promise.all([requestMeasurement("A1"), requestMeasurement("B2"), requestMeasurement("C3")]);
console.log("result:", measurements.map((m) => `${m.station}=${m.value}`).join(" "));
```

```
request started: A1
request started: B2
request started: C3
result: A1=21.4 B2=19.8 C3=23.1
```

All three requests started before `Promise.all` was even called, while the array was
being prepared. The waits **overlapped**; the result arrived once the slowest request,
A1, finished.

The result array preserves **request order**, not arrival order. In the callbacks
topic, achieving this required placing results by index; here, the guarantee belongs
to the combinator itself.

## Wait for All: `Promise.all`

`Promise.all` fulfills, with a result array, once **all** of the given promises have
fulfilled. If any one of them rejects, it rejects **immediately, with the first
rejection**.

```js
const SOURCE = {
  A1: { delay: 30, value: 21.4 },
  B2: { delay: 10, value: 19.8 },
  C3: { delay: 20, value: 23.1 },
  D4: { delay: 15, error: "sensor fault" },
};

function requestMeasurement(station) {
  console.log("request started:", station);
  return new Promise((resolve, reject) => {
    const record = SOURCE[station];
    setTimeout(() => {
      if (record.error) reject(new Error(`${station}: ${record.error}`));
      else resolve({ station, value: record.value });
    }, record.delay);
  });
}

try {
  await Promise.all([requestMeasurement("A1"), requestMeasurement("D4"), requestMeasurement("C3")]);
} catch (error) {
  console.log("all rejected:", error.message);
}
```

```
request started: A1
request started: D4
request started: C3
all rejected: D4: sensor fault
```

Two points matter. First, the result was known at the fifteenth millisecond; A1 and C3
were never waited for. Second, and more important: **not being waited for is not being
canceled.** The A1 and C3 requests keep going, and their results are still produced; no
one just looks at them. Cancellation requires a separate mechanism and will be built
in a later lesson.

This behavior is correct for jobs meaning "all or nothing": the average of three
measurements cannot be computed while one is missing.

## Collect Every Result: `Promise.allSettled`

When a partial result is acceptable, `all` is the wrong tool. `Promise.allSettled`
**never rejects**; it returns a record carrying the status and result of every
promise.

```js
const SOURCE = {
  A1: { delay: 30, value: 21.4 },
  B2: { delay: 10, value: 19.8 },
  C3: { delay: 20, value: 23.1 },
  D4: { delay: 15, error: "sensor fault" },
};

function requestMeasurement(station) {
  console.log("request started:", station);
  return new Promise((resolve, reject) => {
    const record = SOURCE[station];
    setTimeout(() => {
      if (record.error) reject(new Error(`${station}: ${record.error}`));
      else resolve({ station, value: record.value });
    }, record.delay);
  });
}

const results = await Promise.allSettled([requestMeasurement("A1"), requestMeasurement("D4"), requestMeasurement("C3")]);
for (const r of results) {
  if (r.status === "fulfilled") console.log("fulfilled:", r.value.station, r.value.value);
  else console.log("rejected:", r.reason.message);
}
```

```
request started: A1
request started: D4
request started: C3
fulfilled: A1 21.4
rejected: D4: sensor fault
fulfilled: C3 23.1
```

The faulty station did not stop the stream; two valid measurements were obtained, and
the error was recorded too. In areas like a measurement network, where partial data is
useful, this is the default choice.

The records' field names are fixed: a fulfilled record has `status` and `value`; a
rejected record has `status` and `reason`.

## The First to Settle: `Promise.race`

`Promise.race` takes the result of whichever promise settles first — it makes no
distinction between fulfillment and rejection.

```js
const SOURCE = {
  A1: { delay: 30, value: 21.4 },
  B2: { delay: 10, value: 19.8 },
  C3: { delay: 20, value: 23.1 },
  D4: { delay: 15, error: "sensor fault" },
};

function requestMeasurement(station) {
  console.log("request started:", station);
  return new Promise((resolve, reject) => {
    const record = SOURCE[station];
    setTimeout(() => {
      if (record.error) reject(new Error(`${station}: ${record.error}`));
      else resolve({ station, value: record.value });
    }, record.delay);
  });
}

const first = await Promise.race([requestMeasurement("A1"), requestMeasurement("B2"), requestMeasurement("C3")]);
console.log("race result:", first.station, first.value);

try {
  await Promise.race([requestMeasurement("A1"), requestMeasurement("D4")]);
} catch (error) {
  console.log("race rejected:", error.message);
}
```

```
request started: A1
request started: B2
request started: C3
race result: B2 19.8
request started: A1
request started: D4
race rejected: D4: sensor fault
```

In the second race, the promise that settled fastest was the faulty station's; `race`
counted it as the result too. This is why `race` does not mean "take the fastest
**healthy** response."

One warning is needed: promises that lose the race **keep living**. If a promise that
never settles is put inside `race`, that promise and everything it closes over stays
in memory for the life of the program. This is one of the leak types covered in the
memory topic.

## The First to Succeed: `Promise.any`

`Promise.any` ignores rejections and returns the result of the **first to fulfill**. If
all of them reject, it rejects with an aggregate error containing all the reasons.

```js
const SOURCE = {
  A1: { delay: 30, value: 21.4 },
  D4: { delay: 15, error: "sensor fault" },
};

function requestMeasurement(station) {
  console.log("request started:", station);
  return new Promise((resolve, reject) => {
    const record = SOURCE[station];
    setTimeout(() => {
      if (record.error) reject(new Error(`${station}: ${record.error}`));
      else resolve({ station, value: record.value });
    }, record.delay);
  });
}

const firstSuccessful = await Promise.any([requestMeasurement("A1"), requestMeasurement("D4")]);
console.log("any result:", firstSuccessful.station, firstSuccessful.value);

try {
  await Promise.any([requestMeasurement("D4")]);
} catch (error) {
  console.log("any rejected:", error.constructor.name);
  console.log("errors inside:", error.errors.map((e) => e.message).join(", "));
}
```

```
request started: A1
request started: D4
any result: A1 21.4
request started: D4
any rejected: AggregateError
errors inside: D4: sensor fault
```

The faulty station rejected at the fifteenth millisecond; `any` ignored it and waited
for the valid response at the thirtieth millisecond. The aggregate error object
carries the individual reasons in an `errors` array — so no error information is lost.

This combinator is the direct counterpart of the redundancy pattern that requests the
same data from multiple sources and uses whichever works first.

## The Selection Criterion

| Combinator | Fulfills | Rejects | Fits when |
|---|---|---|---|
| `all` | If all fulfill | On the first rejection | Every part is required |
| `allSettled` | Always | Never | A partial result is acceptable |
| `race` | If the first to settle fulfills | If the first to settle rejects | Setting a time limit |
| `any` | On the first to fulfill | If all reject | Redundant sources |

All four take the same input shape and return a single promise; this is why they can
be **nested**. A result chosen with `any` from one set of stations can be combined
with another set's result inside `all`.

## Summary

- Combinators do not start work; the work has started once the promises are created,
  and the combinator only organizes the waiting.
- `all` gives the result array in request order and rejects immediately on the first
  rejection; requests that are not waited for are not canceled, they keep going.
- `allSettled` never rejects; it returns a status-and-result record for every promise.
- `race` looks at whichever settles first and does not distinguish success from
  failure; `any` picks the first to fulfill and, if all reject, carries the reasons in
  an aggregate error.
- Promises that lose the race and never settle continue to occupy memory.

## Next Step

The combinators built the structure of waiting, but they did not change how the code
reads: results are still processed inside callbacks. The next lesson introduces the
syntactic layer that expresses the same chains with sequential-looking syntax —
`async` and `await`. The same lesson shows the common bug where this syntax leads to
unintentional sequential waiting, without the writer noticing, and fixes it with the
combinators built in this lesson.
