Promise Combinators

Reviewed & published by Brayan K

Promise combinators are static methods — Promise.all, allSettled, race, and any — that take an array of promises and combine them into a single promise, letting you coordinate many async operations at once.

Part of the free JavaScript course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

They are the tools you reach for whenever you have several async jobs running together — fetching from five APIs, racing a request against a timeout, or collecting results even when some calls fail.

What You'll Learn in This Lesson

📚 Prerequisites: You should be comfortable with promises and async/await. Combinators build directly on those ideas.

🍽️ Real-World Analogy: Imagine ordering food for a table:

1️⃣ Promise.all — All or Nothing (Parallel)

Promise.all takes an iterable of promises and returns one promise that resolves to an array of all their values — in the same order you passed them, regardless of which finished first. It is fail-fast: if any promise rejects, the combined promise rejects immediately with that error and the other results are discarded.

Because all the promises are already running, the total time is roughly the time of the slowest one — not the sum. That is the parallel speed-up you came for.

Here is the real-world fan-out pattern with fetch:

⚠️ Heads up: One rejection loses every result. If you need the successful ones even when some fail, reach for Promise.allSettled instead.

// A helper that resolves after `ms` with a label
const after = (ms, label) =>
  new Promise((resolve) => setTimeout(() => resolve(label), ms));

async function main() {
  const start = Date.now();

  // All three start NOW and run together
  const results = await Promise.all([
    after(100, "user"),
    after(200, "posts"),
    after(150, "settings"),
  ]);

  const elapsed = Date.now() - start;
  console.log("results:", results);
  // ✅ Expected output: results: [ 'user', 'posts', 'settings' ]

  // Total ≈ the slowest (200ms), NOT 100+200+150 = 450ms
  console.log("ran in parallel:", elapsed < 300);
  // ✅ Expected output: ran in parallel: true
}

main();

2️⃣ Promise.allSettled — Never Gives Up

Promise.allSettled waits for every promise to finish and never rejects. Instead of values, you get an array of result objects: { status: "fulfilled", value } for successes and { status: "rejected", reason } for failures. It is perfect when one slow or broken source should not sink the whole operation.

const ok = (v) => Promise.resolve(v);
const fail = (msg) => Promise.reject(new Error(msg));

async function main() {
  const results = await Promise.allSettled([
    ok("api-A worked"),
    fail("api-B is down"),
    ok("api-C worked"),
  ]);

  for (const r of results) {
    if (r.status === "fulfilled") {
      console.log("✅", r.value);
    } else {
      console.log("❌", r.reason.message);
    }
  }
  // ✅ Expected output:
  // ✅ api-A worked
  // ❌ api-B is down
  // ✅ api-C worked

  const successes = results.filter((r) => r.status === "fulfilled").length;
  console.log("successes:", successes); // ✅ Expected output: successes: 2
}

main();

🎯 Your Turn #1 — Run Two Promises in Parallel

Use Promise.all to await both promises at once and log the array.

const price = Promise.resolve(19.99);
const stock = Promise.resolve(42);

async function main() {
  // 👉 replace ___ with Promise.all so both resolve together
  const [p, s] = await ___([price, stock]);
  console.log(p, s); // ✅ Expected output: 19.99 42
}

main();

3️⃣ Promise.race & Promise.any — First Across the Line

Both settle as soon as a single promise settles, but they disagree about failures. Promise.race takes whichever promise settles first — even if it rejects — which makes it the classic way to bolt a timeout onto slow work. Promise.any ignores rejections and resolves with the first fulfillment; only if every promise rejects does it reject, with an AggregateError whose .errors array holds every reason.

💡 Tip: Use race for timeouts (a fast rejection should win) and any for redundancy (you only need one source to succeed).

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

async function main() {
  // race: the fastest to settle wins, even though it FAILS
  try {
    await Promise.race([
      after(50, "boom", true),  // rejects first
      after(100, "slow-ok"),
    ]);
  } catch (e) {
    console.log("race got:", e.message);
    // ✅ Expected output: race got: boom
  }

  // any: ignores the early failure, takes the first SUCCESS
  const winner = await Promise.any([
    after(50, "boom", true),  // rejects, ignored
    after(100, "slow-ok"),    // first fulfillment
  ]);
  console.log("any got:", winner);
  // ✅ Expected output: any got: slow-ok

  // any: if EVERYTHING fails -> AggregateError
  try {
    await Promise.any([after(20, "x", true), after(40, "y", true)]);
  } catch (err) {
    console.log("all failed:", err.errors.map((e) => e.message));
    // ✅ Expected output: all failed: [ 'x', 'y' ]
  }
}

main();

🎯 Your Turn #2 — Add a Timeout

Use Promise.race so the slow work loses to the 30ms timeout.

const slow = new Promise((res) => setTimeout(() => res("done"), 200));
const timeout = new Promise((_, rej) =>
  setTimeout(() => rej(new Error("too slow")), 30)
);

async function main() {
  try {
    // 👉 replace ___ with Promise.race so the timeout can win
    await ___([slow, timeout]);
  } catch (e) {
    console.log(e.message); // ✅ Expected output: too slow
  }
}

main();

🧠 Quick Recall

Predict the output before revealing the answer.

It rejects with "x". Promise.all is fail-fast — the 1 and 3 never reach you.

"rejected" — allSettled never throws; it reports the failure as a result object.

It rejects with an AggregateError because every promise rejected; .errors is ["a", "b"].

Common Errors & Fixes

🎯 Mini Challenge — Resilient Dashboard Loader

Load three "widgets" in parallel. Two succeed, one fails. Use Promise.allSettled so the dashboard still shows the widgets that loaded and counts the failures.

const widget = (name, ms, fail = false) =>
  new Promise((res, rej) =>
    setTimeout(() => (fail ? rej(new Error(name + " failed")) : res(name)), ms)
  );

async function loadDashboard() {
  const results = await Promise.allSettled([
    widget("chart", 60),
    widget("feed", 40, true),  // this one breaks
    widget("weather", 80),
  ]);

  const loaded = results
    .filter((r) => r.status === "fulfilled")
    .map((r) => r.value);
  const failures = results.filter((r) => r.status === "rejected").length;

  console.log("loaded widgets:", loaded);
  // ✅ Expected output: loaded widgets: [ 'chart', 'weather' ]
  console.log("failures:", failures);
  // ✅ Expected output: failures: 1
}

loadDashboard();

Quick Reference — Combinators

MethodResolves whenRejects when
allall fulfillany one rejects
allSettledall settlenever
racefirst settles (ok)first settles (fail)
anyfirst fulfillsall reject (AggregateError)

📚 Keep Learning

🎉 Lesson Complete — Promise Combinators!

Practice quiz

What does Promise.all resolve to when every promise fulfills?

  • The first value only
  • An array sorted by finish time
  • An array of all values in input order
  • A single combined object

Answer: An array of all values in input order. Promise.all resolves to an array of all values, in the same order you passed them in.

What happens to Promise.all if one input promise rejects?

  • It rejects immediately and discards other results (fail-fast)
  • It ignores the failure
  • It waits for the rest then rejects
  • It resolves with the successes

Answer: It rejects immediately and discards other results (fail-fast). Promise.all is fail-fast: one rejection rejects the whole thing and the other results are lost.

How does Promise.allSettled differ from Promise.all?

  • It rejects faster
  • It only takes two promises
  • It runs them sequentially
  • It never rejects and reports every outcome

Answer: It never rejects and reports every outcome. allSettled waits for every promise and gives an array of { status, value/reason } — it never rejects.

What shape does each Promise.allSettled result object have for a success?

  • { ok: true, data }
  • { status: 'fulfilled', value }
  • { resolved: value }
  • Just the value

Answer: { status: 'fulfilled', value }. Fulfilled entries are { status: 'fulfilled', value }; failures are { status: 'rejected', reason }.

Which combinator is the classic choice for adding a timeout to slow work?

  • Promise.race
  • Promise.all
  • Promise.allSettled
  • Promise.any

Answer: Promise.race. Promise.race settles with the first promise to settle (even a rejection), so a timeout rejection can win.

How does Promise.any treat rejections?

  • It rejects on the first rejection
  • It collects them into an array of values
  • It ignores rejections and resolves with the first fulfillment
  • It retries them

Answer: It ignores rejections and resolves with the first fulfillment. Promise.any resolves with the first fulfillment, ignoring rejections unless they all reject.

What does Promise.any reject with when every input promise rejects?

  • The last error
  • An AggregateError holding all the reasons
  • undefined
  • A TypeError

Answer: An AggregateError holding all the reasons. If all reject, Promise.any rejects with an AggregateError whose .errors array holds every reason.

Do the combinators start your promises running?

  • Yes, they trigger the work
  • Only Promise.all does
  • Only with await
  • No — promises run when created; the combinators only wait

Answer: No — promises run when created; the combinators only wait. Promises start the moment they are created; combinators merely coordinate when to wait on them.

What is the result of await Promise.all([1, Promise.reject('x'), 3])?

  • [1, 'x', 3]
  • It rejects with 'x'
  • [1, 3]
  • It resolves with undefined

Answer: It rejects with 'x'. Promise.all is fail-fast, so the rejection 'x' rejects the whole call and 1 and 3 are discarded.

Which mistake turns parallel work into accidental sequential work?

  • Using Promise.all(urls.map(fetch))
  • Passing already-running promises
  • Awaiting each fetch inside a for loop
  • Using allSettled

Answer: Awaiting each fetch inside a for loop. Awaiting one fetch before starting the next serializes them; map them into Promise.all to overlap.

Continue this course

Frequently asked questions

What is the difference between Promise.all and Promise.allSettled?

Promise.all is fail-fast: the moment any input promise rejects, the whole thing rejects with that error and you lose the other results. Promise.allSettled never rejects — it waits for every promise to finish and gives you an array describing each outcome ({ status: 'fulfilled', value } or { status: 'rejected', reason }). Use all when every task must succeed; use allSettled when you want partial results.

When should I use Promise.race versus Promise.any?

Both settle as soon as ONE promise settles, but they differ on what counts. Promise.race settles with the first promise to settle either way — so a fast rejection wins. Promise.any ignores rejections and resolves with the first fulfillment, only rejecting (with an AggregateError) if every promise rejects. Use race for timeouts; use any for 'give me the first source that works'.

Do the combinators run my promises in parallel?

The combinators do not start anything — your promises are already running the moment you create them. The combinators simply wait. So if you create all your promises up front and then pass the array to Promise.all, the work overlaps (parallel). If you await each one in a loop before creating the next, you have made them sequential by accident.

What is an AggregateError?

AggregateError is a special error type that wraps several errors at once. Promise.any throws one when all of its input promises reject, exposing every individual reason on its .errors array. It is the only built-in combinator that can produce one, and it lets you inspect why every source failed instead of seeing just a single message.

Related lessons