Skip to content
ExponaBLOG
JAVASCRIPT & TYPESCRIPT · DAY 020 / 100

Promise.all, allSettled, race and any: a practical guide

Choosing the right combinator.

Expona Admin7 min read

Promise.all, allSettled, race and any: Choosing the Right Combinator

When you're juggling multiple asynchronous operations in JavaScript, reaching for the wrong Promise combinator can mean the difference between a robust system and one that silently fails or hangs. I've shipped production features across e-commerce platforms, trading systems, and APIs where picking the right tool—all, allSettled, race, or any—made or broke user experience. Today we're going deep into when and why you use each one.

Combinators: A Quick Reference

Let me start with a mental model that stuck with me. Each combinator has a different failure mode and completion trigger. Here's the landscape:

Method Resolves When Rejects When Best For
Promise.all All input promises fulfill Any promise rejects All-or-nothing workflows
Promise.allSettled All input promises settle (resolve or reject) Never rejects Batch operations with partial failures
Promise.race First promise settles (any outcome) If the first to settle rejects Timeouts and first-to-respond
Promise.any First promise fulfills All promises reject Fallback strategies and resilience

That table is your cheat sheet, but understanding why each exists is what separates shipping confidently from debugging race conditions at 2 AM.

all for Dependencies: Every Result Matters

I use Promise.all when I need every single operation to succeed. If any fail, the whole thing fails. It's the "all-or-nothing" combinator.

Real scenario: You're building a checkout flow. You need to fetch user data, inventory levels, and shipping rates. All three are required before showing the summary. If any one fails, the checkout can't proceed—so you fail fast.

typescript
async function prepareCheckout(userId: string, cartItems: string[]) {
  try {
    const [user, inventory, shipping] = await Promise.all([
      fetchUser(userId),
      checkInventory(cartItems),
      fetchShippingRates(cartItems),
    ]);

    return {
      user,
      inventory,
      shipping,
      ready: true,
    };
  } catch (error) {
    console.error('Checkout prep failed:', error);
    // User sees a single error: something went wrong
    throw new Error('Unable to prepare checkout');
  }
}

The beauty here is simplicity. One array of promises in, one array of results out (in the same order). If the inventory check fails—maybe an item sold out—the whole checkout prep rejects. You don't waste time fetching shipping if inventory is gone.

The trade-off: You're abandoning partial success. Sometimes that's correct. But if you're fetching analytics data from five sources and one is flaky, Promise.all will nuke your entire request. That's where allSettled comes in.

allSettled for Batches: Collect What You Can

Promise.allSettled waits for every promise to finish—whether they resolve or reject—and gives you the full picture. It never rejects. You always get a result.

I reach for this when processing batches where partial failures are acceptable. A data migration importing 10,000 user records. A batch email send. A multi-tenant sync where a few customers can fail without blocking the rest.

typescript
async function sendNotificationBatch(userIds: string[]) {
  const promises = userIds.map(id => sendNotification(id));
  
  const results = await Promise.allSettled(promises);
  
  const successes = results.filter(
    (r): r is PromiseSettledResult & { status: 'fulfilled' } =>
      r.status === 'fulfilled'
  );
  
  const failures = results.filter(
    (r): r is PromiseRejectedResult =>
      r.status === 'rejected'
  );

  console.log(
    `Sent ${successes.length}/${userIds.length}. ` +
    `Failed: ${failures.length}`
  );

  if (failures.length > 0) {
    await logFailures(failures.map(f => f.reason));
  }

  return {
    sent: successes.length,
    failed: failures.length,
  };
}

Notice the result shape: each item is { status, value } or { status, reason }. You inspect the status to know what happened. No try-catch needed—everything succeeds structurally, even if some operations failed.

Where I've used this in production: A ride-hailing platform syncing driver documents across multiple verification providers. One provider's API going down shouldn't block the sync. We report which documents failed, retry them separately, and keep the system moving.

race for Timeouts: First to Respond Wins

Promise.race resolves as soon as the first promise settles—regardless of whether it resolved or rejected. This is your timeout weapon.

Classic pattern: wrap your request in a race against a timer. If the request wins, you get the data. If the timer wins, you abort.

typescript
function withTimeout(
  promise: Promise,
  ms: number,
  label: string = 'Operation'
): Promise {
  return Promise.race([
    promise,
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms)
    ),
  ]);
}

// Usage
async function fetchUserWithTimeout(userId: string) {
  try {
    const user = await withTimeout(
      fetchUser(userId),
      5000,
      'User fetch'
    );
    return user;
  } catch (error) {
    console.error('Timed out or failed:', error);
    throw error;
  }
}

I've used this pattern everywhere: API requests that can't hang, WebSocket connections that need a fallback, long-running computations that must complete within a window.

Important caveat: Promise.race doesn't cancel the loser. If your timer wins, the original promise is still running in the background. If it's an HTTP request, it's still going. You need to actually abort it (using AbortController) if you want to clean up:

typescript
async function fetchWithRealTimeout(
  url: string,
  timeoutMs: number
): Promise {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);

  try {
    const response = await fetch(url, { signal: controller.signal });
    clearTimeout(timeoutId);
    return response;
  } catch (error) {
    clearTimeout(timeoutId);
    throw error;
  }
}

This is cleaner and actually cancels the fetch. Always use AbortController with race in real code.

any: First Success, Multiple Fallbacks

Promise.any is the optimistic cousin of all. It resolves as soon as one promise fulfills. It only rejects if all promises reject.

I use this for resilience patterns. Multiple data sources, pick whichever responds first. Multiple CDNs, use whichever is fastest. Fallback chains.

typescript
async function fetchUserData(userId: string) {
  try {
    const user = await Promise.any([
      fetchFromPrimaryDB(userId),
      fetchFromCache(userId),
      fetchFromBackupDB(userId),
    ]);
    return user;
  } catch (error) {
    // AggregateError: all sources failed
    throw new Error('User not found in any source');
  }
}

The moment any source succeeds, you win and move on. The slower sources keep running, but you don't wait. This is powerful for building fault-tolerant systems.

Real example from a trading platform: Price quotes from three liquidity providers. We'd race all three and take the first valid quote. If all three timeout or error, that's a hard failure—we can't execute without a quote.

Common Mistakes

  • Using all when you mean allSettled: A single failing operation nukes the entire batch. If you're importing data, you probably want partial success. Trap: you discover this in production when one record failure blocks 10,000 others.
  • Forgetting to clean up after race: The loser keeps running. A fetch request that lost the race is still consuming bandwidth. Use AbortController.
  • Not handling any's AggregateError correctly: When all promises reject, you get an AggregateError with an errors array. If you log it as a normal error, you lose the details. Check the errors property.
  • Mixing metaphors: Don't use race for "whichever succeeds first"—that's any. race is for "first to settle," which could be a rejection. Easy to confuse.
  • Not preserving order with all: This is rarely wrong since all returns results in input order, but if you're transforming the promise array, double-check the mapping. One off-by-one error and your results align with the wrong operations.

Key Takeaways

  • Use Promise.all for all-or-nothing workflows where every operation must succeed.
  • Use Promise.allSettled for batch operations where partial failure is acceptable and you need to inspect what failed.
  • Use Promise.race with AbortController to implement timeouts and cancel losers.
  • Use Promise.any for resilience patterns: multiple sources, pick the first success, fail only if all reject.
  • Remember the order: all is strict (one rejection fails all), allSettled is lenient (waits for all, never rejects), race is urgent (first to settle wins), any is opportunistic (first to succeed wins).
  • Always pair race with actual cancellation (AbortController) to avoid resource leaks.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles