JavaScript Promise.all, Promise.race, Promise.allSettled
When building modern JavaScript applications, you'll frequently need to coordinate multiple asynchronous operations. Maybe you're fetching data from several API endpoints, uploading multiple files,...
Key Insights
Promise.allfails fast on the first rejection, making it ideal for operations where all results are required, whilePromise.allSettledwaits for everything to complete regardless of success or failure, perfect for batch operations where partial failures are acceptable.Promise.racesettles with the first promise that completes, enabling powerful patterns like request timeouts, fallback servers, and competitive API calls where you only need the fastest response.- Combining these methods unlocks advanced patterns: wrap
Promise.allinPromise.racefor all-or-nothing operations with timeouts, or usePromise.allSettledwith filtering to process successful results while logging failures separately.
Introduction to Promise Combinators
When building modern JavaScript applications, you’ll frequently need to coordinate multiple asynchronous operations. Maybe you’re fetching data from several API endpoints, uploading multiple files, or validating several inputs concurrently. Handling these operations individually leads to verbose, error-prone code.
Promise combinators solve this coordination problem. They’re methods that take multiple promises and return a single promise with specific settlement behavior. Let’s see the difference:
// Without combinators - verbose and awkward
async function fetchUserData(userId) {
const profile = await fetch(`/api/users/${userId}/profile`);
const posts = await fetch(`/api/users/${userId}/posts`);
const followers = await fetch(`/api/users/${userId}/followers`);
return {
profile: await profile.json(),
posts: await posts.json(),
followers: await followers.json()
};
}
// With Promise.all - clean and concurrent
async function fetchUserData(userId) {
const [profile, posts, followers] = await Promise.all([
fetch(`/api/users/${userId}/profile`).then(r => r.json()),
fetch(`/api/users/${userId}/posts`).then(r => r.json()),
fetch(`/api/users/${userId}/followers`).then(r => r.json())
]);
return { profile, posts, followers };
}
The second version runs all requests concurrently and is significantly faster. But each combinator has different behavior when promises reject or resolve at different times.
Promise.all - Wait for All or Fail Fast
Promise.all takes an array of promises and returns a single promise that resolves when all input promises resolve, or rejects immediately when any input promise rejects. This “fail fast” behavior is crucial—the returned promise doesn’t wait for remaining promises once one fails.
async function loadDashboard() {
try {
const [userData, analytics, notifications] = await Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/analytics').then(r => r.json()),
fetch('/api/notifications').then(r => r.json())
]);
return { userData, analytics, notifications };
} catch (error) {
console.error('Failed to load dashboard:', error);
// One failure means we can't show the dashboard
throw new Error('Dashboard unavailable');
}
}
The fail-fast behavior makes sense for operations where all results are required. If you can’t load user data, there’s no point waiting for analytics and notifications.
Here’s what happens with a rejection:
const promise1 = Promise.resolve(1);
const promise2 = new Promise((resolve) => setTimeout(() => resolve(2), 2000));
const promise3 = Promise.reject(new Error('Failed'));
const promise4 = new Promise((resolve) => setTimeout(() => resolve(4), 3000));
Promise.all([promise1, promise2, promise3, promise4])
.then(results => console.log(results))
.catch(error => console.error('Error:', error.message));
// Output immediately: "Error: Failed"
// promise2 and promise4 continue running, but their results are ignored
Key point: Promise.all rejects immediately upon the first rejection, but doesn’t cancel the other promises. They continue executing in the background, which matters for operations with side effects.
A practical pattern is parallel validation:
async function validateForm(formData) {
const validations = await Promise.all([
validateEmail(formData.email),
validatePassword(formData.password),
checkUsernameAvailable(formData.username),
validateAge(formData.age)
]);
return validations.every(v => v.isValid);
}
If any validation fails (throws an error), the entire form is invalid, and you can short-circuit immediately.
Promise.race - First to Finish Wins
Promise.race returns a promise that settles (resolves or rejects) as soon as the first promise in the array settles. The value or reason from that first settled promise becomes the result.
The most common use case is implementing timeouts:
function fetchWithTimeout(url, timeout = 5000) {
return Promise.race([
fetch(url),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Request timeout')), timeout)
)
]);
}
// Usage
try {
const response = await fetchWithTimeout('/api/slow-endpoint', 3000);
const data = await response.json();
} catch (error) {
if (error.message === 'Request timeout') {
console.error('Request took too long');
}
}
Another powerful pattern is fallback servers:
async function fetchFromFastestServer(endpoint) {
const servers = [
'https://api-us-east.example.com',
'https://api-us-west.example.com',
'https://api-eu.example.com'
];
const response = await Promise.race(
servers.map(server => fetch(`${server}${endpoint}`))
);
return response.json();
}
This sends requests to all servers simultaneously and uses whichever responds first. The user gets the fastest possible response based on their location and current server load.
A more sophisticated timeout pattern that cleans up:
function fetchWithAbortTimeout(url, timeout = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
return fetch(url, { signal: controller.signal })
.finally(() => clearTimeout(timeoutId));
}
This actually cancels the fetch request when the timeout occurs, preventing wasted bandwidth and server resources.
Promise.allSettled - Wait for All Regardless
Promise.allSettled waits for all promises to settle (either resolve or reject) and returns an array of objects describing each outcome. This is perfect for batch operations where you need to know what succeeded and what failed.
async function processMultipleUploads(files) {
const uploadPromises = files.map(file => uploadFile(file));
const results = await Promise.allSettled(uploadPromises);
const successful = results.filter(r => r.status === 'fulfilled');
const failed = results.filter(r => r.status === 'rejected');
console.log(`${successful.length} uploads succeeded`);
console.log(`${failed.length} uploads failed`);
return {
successful: successful.map(r => r.value),
failed: failed.map(r => ({ reason: r.reason.message }))
};
}
The result structure is consistent:
const results = await Promise.allSettled([
Promise.resolve(42),
Promise.reject(new Error('Failed')),
Promise.resolve('success')
]);
console.log(results);
// [
// { status: 'fulfilled', value: 42 },
// { status: 'rejected', reason: Error: Failed },
// { status: 'fulfilled', value: 'success' }
// ]
This is invaluable for operations like sending notifications to multiple users where some failures shouldn’t prevent others from succeeding:
async function sendBulkNotifications(users, message) {
const results = await Promise.allSettled(
users.map(user => sendNotification(user.id, message))
);
const failures = results
.filter(r => r.status === 'rejected')
.map((r, index) => ({ user: users[index], error: r.reason }));
if (failures.length > 0) {
await logFailedNotifications(failures);
}
return {
sent: results.filter(r => r.status === 'fulfilled').length,
failed: failures.length
};
}
Comparison and When to Use Each
| Method | Resolves When | Rejects When | Use Case |
|---|---|---|---|
Promise.all |
All promises resolve | Any promise rejects (immediately) | All results required; one failure means total failure |
Promise.race |
First promise settles | First promise rejects | Need fastest result; timeouts; fallbacks |
Promise.allSettled |
All promises settle | Never rejects | Need all results; partial failures acceptable |
Here’s the same scenario implemented three ways:
const promises = [
fetch('/api/data1').then(r => r.json()),
fetch('/api/data2').then(r => r.json()),
Promise.reject(new Error('API 3 failed')),
fetch('/api/data4').then(r => r.json())
];
// Promise.all - rejects immediately
try {
const results = await Promise.all(promises);
} catch (error) {
console.log('Promise.all failed:', error.message);
// Output: "Promise.all failed: API 3 failed"
}
// Promise.race - returns first settled (the rejection)
try {
const result = await Promise.race(promises);
} catch (error) {
console.log('Promise.race failed:', error.message);
// Output: "Promise.race failed: API 3 failed"
}
// Promise.allSettled - waits for all, never rejects
const results = await Promise.allSettled(promises);
console.log('Promise.allSettled results:', results.length);
// Output: "Promise.allSettled results: 4"
console.log('Successful:', results.filter(r => r.status === 'fulfilled').length);
Real-World Patterns and Best Practices
Combining methods for powerful patterns:
// Promise.all with timeout
async function fetchAllWithTimeout(urls, timeout = 5000) {
const fetchPromises = urls.map(url => fetch(url).then(r => r.json()));
return Promise.race([
Promise.all(fetchPromises),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), timeout)
)
]);
}
Error recovery with fallbacks:
async function fetchWithFallback(primaryUrl, fallbackUrl) {
try {
return await fetch(primaryUrl).then(r => r.json());
} catch (error) {
console.warn('Primary failed, trying fallback');
return await fetch(fallbackUrl).then(r => r.json());
}
}
Dynamic promise arrays:
async function processInBatches(items, batchSize = 10) {
const results = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const batchResults = await Promise.allSettled(
batch.map(item => processItem(item))
);
results.push(...batchResults);
}
return results;
}
Performance consideration: Don’t use Promise.all for hundreds of concurrent operations. Batch them to avoid overwhelming servers or exhausting connection pools.
Choose Promise.all when you need all results and one failure means the operation failed. Choose Promise.race for timeouts and competitive requests. Choose Promise.allSettled for batch operations where you need to handle partial failures gracefully. Understanding these differences will make your async code more robust and predictable.