DEV Community

Cover image for You reach for `Promise.all` for every concurrent request. Here's when to use the other three.
Parsa Jiravand
Parsa Jiravand

Posted on

You reach for `Promise.all` for every concurrent request. Here's when to use the other three.

Imagine you're loading a dashboard. Four widgets, four APIs, fire them all at once:

const [users, revenue, alerts, activity] = await Promise.all([
  fetchUsers(),
  fetchRevenue(),
  fetchAlerts(),
  fetchActivity(),
]);
Enter fullscreen mode Exit fullscreen mode

The alerts API is occasionally slow and sometimes returns a 500. When it does, your entire dashboard fails. Not one broken widget — four broken widgets. Promise.all rejects on the first failure and takes the other three successful results with it into the void.

You reached for the right primitive for concurrency, but the wrong one for this use case.

The four methods and what they actually do

JavaScript gives you four ways to run promises concurrently. They differ in one thing: what happens when a promise fails or resolves first.

Method Resolves when Rejects when
Promise.all All succeed Any one fails
Promise.allSettled All finish (success or failure) Never
Promise.any Any one succeeds All fail
Promise.race Any one finishes Any one fails first

The instinct to reach for Promise.all is understandable — it returns all the values in a single array and feels like the obvious way to "do these things at the same time." But concurrency and failure handling are two separate questions. Promise.all answers both at once, and the answer to the second one is often wrong for the situation you're in.

🎮 Try it yourself

▶️ Open the interactive playground →

Runs right in your browser — poke at it and watch the concept react live.

Promise.allSettled — partial success

Promise.allSettled waits for every promise to settle — resolve or reject — and returns a result array describing what happened to each one:

const results = await Promise.allSettled([
  fetchUsers(),
  fetchRevenue(),
  fetchAlerts(),
  fetchActivity(),
]);

for (const result of results) {
  if (result.status === 'fulfilled') {
    console.log('got data:', result.value);
  } else {
    console.error('failed:', result.reason);
  }
}
Enter fullscreen mode Exit fullscreen mode

Each element has a status of 'fulfilled' (with a value) or 'rejected' (with a reason). No promise can cancel any other. This is the right primitive for your dashboard: three working widgets stay working even when the fourth API is down.

The typical UI pattern is to destructure and null-check independently:

const [usersResult, revenueResult] = await Promise.allSettled([
  fetchUsers(),
  fetchRevenue(),
]);

const users = usersResult.status === 'fulfilled' ? usersResult.value : null;
const revenue = revenueResult.status === 'fulfilled' ? revenueResult.value : null;
Enter fullscreen mode Exit fullscreen mode

Render each widget or its error state based on its own result. One API being down no longer bleeds into another widget's data.

Promise.any — fastest successful response

Promise.any resolves with the value of whichever promise succeeds first. It only rejects if all promises fail — and even then, the rejection is an AggregateError that wraps all the individual failure reasons.

The use case is CDN failover: you have two or three endpoints and want the fastest response that actually works.

const data = await Promise.any([
  fetch('https://cdn1.example.com/data.json'),
  fetch('https://cdn2.example.com/data.json'),
  fetch('https://cdn3.example.com/data.json'),
]).then(res => res.json());
Enter fullscreen mode Exit fullscreen mode

If cdn1 is slow and cdn2 responds first, you get cdn2's result immediately. If cdn1 responds later, it's discarded. If cdn1 and cdn2 both fail but cdn3 succeeds, you still get data.

The key distinction from Promise.race: a rejection from a fast server doesn't end Promise.any. It continues waiting for a success from the remaining sources. Promise.race stops at the first settled promise — a rejection is enough to close it. Promise.any stops at the first resolved promise. The two primitives are not interchangeable, and reaching for race when you want any gets you a CDN failover that fails as soon as your primary CDN sends any error.

Promise.race — timeout pattern

Promise.race settles with whatever promise settles first — success or failure. The correct use case: imposing a timeout on an operation that doesn't have one built in.

function withTimeout(promise, ms) {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error(`Timed out after ${ms}ms`)), ms)
  );
  return Promise.race([promise, timeout]);
}

const data = await withTimeout(fetchLargeReport(), 5000);
Enter fullscreen mode Exit fullscreen mode

If fetchLargeReport() resolves within 5 seconds, you get the data. If it takes longer, the timeout promise rejects first and Promise.race rejects with the timeout error.

Note that the original fetch continues running in the background after the race completes — the promise doesn't stop just because you stopped listening. Pair this with AbortController to actually cancel the request when the timeout fires.

When Promise.all is the right choice

Promise.all is not wrong — it solves a real and specific problem: operations where you need all results and partial success is not a meaningful state.

A form submission that must write to three services atomically: if any fails, the whole operation should fail and roll back. Fetching a user record and their permissions when you can't render the page with either one missing. Building a combined response from two sources where neither source alone produces a valid output.

The tell is whether partial success matters to your application. If one failure should stop everything, Promise.all is exactly right. If partial success is valid — and for most UI data fetching, it is — you want allSettled.

🧠 Test yourself

Think it clicked? Take the 6-question quiz →

Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.

The decision rule

Before you write Promise.all, ask one question:

If one of these promises fails, should the others still resolve?

  • Yes, and I want all the results: Promise.allSettled
  • Yes, and I just want the fastest one that works: Promise.any
  • I want the first outcome, success or failure: Promise.race
  • No — one failure should fail everything: Promise.all

Promise.all was the first concurrent Promise method, so it's the one most developers reach for first. But the others aren't edge cases — they describe real, common failure modes that Promise.all handles badly. Next time you type Promise.all, spend one second on that question. You might mean one of the other three.


Thanks for reading! Let's stay connected:

Top comments (0)