Skip to content
Codeloom
JavaScript

JavaScript Promise Combinators: all, allSettled, race, and any

Master all four JavaScript Promise combinators -- Promise.all, allSettled, race, and any -- with practical patterns for concurrency, timeouts, and resilience.

·7 min read · By Codeloom
Intermediate 10 min read

What you'll learn

  • How each Promise combinator behaves with successes and failures
  • Choosing the right combinator for each concurrency scenario
  • Building practical patterns like timeouts, retries, and parallel pipelines

Prerequisites

  • JavaScript Promises and async/await
  • Understanding of the event loop

JavaScript provides four static methods on Promise for composing multiple asynchronous operations. Each has different semantics for handling success and failure. Choosing the right one is critical for writing correct concurrent code.

Quick comparison

CombinatorResolves whenRejects when
Promise.allAll promises fulfillAny promise rejects
Promise.allSettledAll promises settleNever rejects
Promise.raceFirst promise settlesFirst promise rejects
Promise.anyFirst promise fulfillsAll promises reject

Promise.all

Waits for every promise to fulfill. If any promise rejects, the entire result rejects immediately.

const [users, products, orders] = await Promise.all([
  fetch("/api/users").then((r) => r.json()),
  fetch("/api/products").then((r) => r.json()),
  fetch("/api/orders").then((r) => r.json()),
]);

console.log(users, products, orders);

Fail-fast behavior

const results = Promise.all([
  new Promise((resolve) => setTimeout(() => resolve("A"), 100)),
  new Promise((_, reject) => setTimeout(() => reject(new Error("B failed")), 50)),
  new Promise((resolve) => setTimeout(() => resolve("C"), 200)),
]);

try {
  await results;
} catch (error) {
  console.log(error.message); // "B failed"
  // Promises A and C still run, but their results are discarded
}

This fail-fast behavior is a feature when all results are required together. If one fails, there is no point in waiting for the others.

Concurrency limiting with Promise.all

Promise.all starts all promises simultaneously. For large arrays, this can overwhelm a server or API. Use a pool pattern to limit concurrency.

async function mapWithConcurrency(items, fn, concurrency = 5) {
  const results = [];
  const executing = new Set();

  for (const [index, item] of items.entries()) {
    const promise = fn(item, index).then((result) => {
      executing.delete(promise);
      return result;
    });

    executing.add(promise);
    results.push(promise);

    if (executing.size >= concurrency) {
      await Promise.race(executing);
    }
  }

  return Promise.all(results);
}

// Fetch 100 URLs, but only 5 at a time
const urls = Array.from({ length: 100 }, (_, i) => `/api/items/${i}`);

const data = await mapWithConcurrency(
  urls,
  async (url) => {
    const res = await fetch(url);
    return res.json();
  },
  5
);

Promise.allSettled

Waits for every promise to settle (fulfill or reject), never short-circuits. Returns an array of result objects with a status field.

const results = await Promise.allSettled([
  fetch("/api/critical").then((r) => r.json()),
  fetch("/api/optional").then((r) => r.json()),
  fetch("/api/analytics").then((r) => r.json()),
]);

for (const result of results) {
  if (result.status === "fulfilled") {
    console.log("Success:", result.value);
  } else {
    console.log("Failed:", result.reason.message);
  }
}

When to use allSettled

Use allSettled when you want to attempt multiple operations and handle each result independently — for example, sending notifications to multiple channels where some might fail.

async function notifyAll(message) {
  const results = await Promise.allSettled([
    sendEmail(message),
    sendSlack(message),
    sendSMS(message),
  ]);

  const failures = results
    .filter((r) => r.status === "rejected")
    .map((r) => r.reason);

  if (failures.length > 0) {
    console.warn(`${failures.length} notification(s) failed:`, failures);
  }

  return {
    totalSent: results.filter((r) => r.status === "fulfilled").length,
    totalFailed: failures.length,
  };
}

Promise.race

Settles as soon as the first promise settles — whether it fulfills or rejects.

const result = await Promise.race([
  fetch("/api/primary-server").then((r) => r.json()),
  fetch("/api/backup-server").then((r) => r.json()),
]);
// Returns whichever response arrives first

Timeout pattern

The most common use of Promise.race: racing a promise against a timer.

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

try {
  const data = await withTimeout(
    fetch("/api/slow-endpoint").then((r) => r.json()),
    5000
  );
  console.log(data);
} catch (error) {
  console.error(error.message); // "Timed out after 5000ms"
}

First response wins

For redundant requests where you want the fastest response:

async function fetchFromFastest(urls) {
  return Promise.race(
    urls.map((url) =>
      fetch(url).then((response) => {
        if (!response.ok) throw new Error(`${url}: ${response.status}`);
        return response.json();
      })
    )
  );
}

const data = await fetchFromFastest([
  "https://cdn1.example.com/data.json",
  "https://cdn2.example.com/data.json",
  "https://cdn3.example.com/data.json",
]);

Caution: if the fastest response is a rejection, Promise.race rejects even if slower promises would succeed. Use Promise.any if you want the first success.

Promise.any

Resolves with the first promise that fulfills. Only rejects if all promises reject, with an AggregateError containing all rejection reasons.

try {
  const data = await Promise.any([
    fetch("https://primary.example.com/api").then((r) => r.json()),
    fetch("https://secondary.example.com/api").then((r) => r.json()),
    fetch("https://tertiary.example.com/api").then((r) => r.json()),
  ]);
  console.log("Got data from fastest succeeding server:", data);
} catch (error) {
  // AggregateError -- only if ALL three failed
  console.error("All servers failed:", error.errors);
}

Resilient service calls

async function resilientFetch(url, retries = 3) {
  const attempts = Array.from({ length: retries }, (_, i) =>
    new Promise((resolve) =>
      setTimeout(() => resolve(fetch(url)), i * 1000)
    ).then((response) => {
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      return response.json();
    })
  );

  return Promise.any(attempts);
}

// Tries 3 times with 1-second delays, returns the first success
const data = await resilientFetch("/api/unreliable-endpoint");

Combining combinators

Real-world scenarios often benefit from combining multiple combinators.

Parallel groups with independent error handling

async function loadDashboard() {
  // Critical data -- must all succeed
  const criticalData = Promise.all([
    fetchUser(),
    fetchPermissions(),
  ]);

  // Optional data -- best effort
  const optionalData = Promise.allSettled([
    fetchNotifications(),
    fetchRecommendations(),
    fetchAnalytics(),
  ]);

  const [critical, optional] = await Promise.all([
    criticalData,
    optionalData,
  ]);

  const [user, permissions] = critical;
  const extras = Object.fromEntries(
    optional
      .filter((r) => r.status === "fulfilled")
      .map((r) => [r.value.type, r.value.data])
  );

  return { user, permissions, ...extras };
}

Race with fallback chain

async function fetchWithFallbacks(primaryUrl, fallbackUrls) {
  try {
    // Try primary first with a timeout
    return await withTimeout(
      fetch(primaryUrl).then((r) => r.json()),
      3000
    );
  } catch {
    // Primary failed or timed out -- race fallbacks
    return Promise.any(
      fallbackUrls.map((url) => fetch(url).then((r) => r.json()))
    );
  }
}

const data = await fetchWithFallbacks(
  "https://primary.api.com/data",
  [
    "https://fallback1.api.com/data",
    "https://fallback2.api.com/data",
  ]
);

Common mistakes

Not handling Promise.all rejections

// Bad: unhandled rejection if any fetch fails
const data = await Promise.all(urls.map((u) => fetch(u)));

// Good: handle the rejection or use allSettled
try {
  const data = await Promise.all(urls.map((u) => fetch(u)));
} catch (error) {
  console.error("One or more fetches failed:", error);
}

Ignoring that race does not cancel losers

// The losing fetch still completes -- it just gets ignored
const result = await Promise.race([fetchA(), fetchB()]);
// Both requests still consume server resources
// Use AbortController to actually cancel losers

Empty arrays

await Promise.all([]);        // Resolves immediately with []
await Promise.allSettled([]);  // Resolves immediately with []
await Promise.race([]);       // NEVER settles (hangs forever)
await Promise.any([]);         // Rejects with AggregateError

Summary

Each Promise combinator serves a distinct concurrency pattern:

  • Promise.all — use when all results are required and any failure should abort. Ideal for loading related data that only makes sense together.
  • Promise.allSettled — use when you want to attempt everything and handle results individually. Ideal for notifications, batch operations, and graceful degradation.
  • Promise.race — use when you want the first result, whether success or failure. Ideal for timeouts and latency-sensitive scenarios.
  • Promise.any — use when you want the first success and can tolerate individual failures. Ideal for redundant requests and fallback chains.

Understanding these four combinators and knowing when to apply each one is essential for writing robust asynchronous JavaScript.