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.
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
| Combinator | Resolves when | Rejects when |
|---|---|---|
Promise.all | All promises fulfill | Any promise rejects |
Promise.allSettled | All promises settle | Never rejects |
Promise.race | First promise settles | First promise rejects |
Promise.any | First promise fulfills | All 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.
Related articles
- JavaScript JavaScript Event Loop Explained: How Async Code Really Works
Understand how the JavaScript event loop handles async operations including the call stack, microtasks, macrotasks, and execution order with practical examples.
- JavaScript JavaScript Error Handling Patterns and Best Practices
Master JavaScript error handling with custom error classes, async error patterns, Result types, error boundaries, and strategies for robust production code.
- JavaScript JavaScript Event Loop, Microtasks, and Macrotasks Explained
Visualize how the JavaScript event loop works -- understand the call stack, microtask queue, macrotask queue, and execution order.
- JavaScript JavaScript Promises Deep Dive
Go beyond the basics of JavaScript promises — explore the microtask queue, error propagation, cancellation patterns, and advanced combinators like any and allSettled.