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.
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.
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.
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:
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.
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
allwhen you meanallSettled: 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. UseAbortController. - Not handling
any's AggregateError correctly: When all promises reject, you get anAggregateErrorwith anerrorsarray. If you log it as a normal error, you lose the details. Check the errors property. - Mixing metaphors: Don't use
racefor "whichever succeeds first"—that'sany.raceis for "first to settle," which could be a rejection. Easy to confuse. - Not preserving order with
all: This is rarely wrong sinceallreturns 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.allfor all-or-nothing workflows where every operation must succeed. - Use
Promise.allSettledfor batch operations where partial failure is acceptable and you need to inspect what failed. - Use
Promise.racewithAbortControllerto implement timeouts and cancel losers. - Use
Promise.anyfor resilience patterns: multiple sources, pick the first success, fail only if all reject. - Remember the order:
allis strict (one rejection fails all),allSettledis lenient (waits for all, never rejects),raceis urgent (first to settle wins),anyis opportunistic (first to succeed wins). - Always pair
racewith actual cancellation (AbortController) to avoid resource leaks.






Comments
Loading comments…