Difficulty: Intermediate
Promise combinators are static methods on the Promise constructor that take an iterable of promises and return a single promise that represents the combined result. They are essential tools for managing multiple concurrent asynchronous operations. JavaScript provides four combinators, each with distinct behavior: Promise.all(), Promise.race(), Promise.allSettled(), and Promise.any(). Choosing the right combinator depends on your use case - whether you need all results, the fastest result, all outcomes regardless of success, or the first success.
Promise.all() takes an iterable of promises and returns a single promise that resolves to an array of all the resolved values, in the same order as the input. If any promise rejects, Promise.all() immediately rejects with that rejection reason - it does not wait for the remaining promises to settle. This "fail-fast" behavior makes it ideal when all results are needed and a single failure means the entire operation should be considered failed. Common use cases include loading multiple API resources that are all required for a page, or running multiple database queries that must all succeed.
Promise.race() returns a promise that settles as soon as any one of the input promises settles - whether it resolves or rejects. The result or error of the first settled promise becomes the result or error of the race. The remaining promises continue to execute in the background but their results are ignored. This is useful for implementing timeouts (race a fetch against a timer) or for redundant requests where you want the fastest response.
Promise.allSettled() waits for all promises to settle, regardless of whether they resolve or reject. It returns an array of objects, each with a status property ('fulfilled' or 'rejected') and either a value or reason property. Unlike Promise.all(), it never short-circuits on a rejection. This is ideal when you want to attempt multiple independent operations and handle each result individually - for example, sending notifications through multiple channels (email, SMS, push) and logging which ones succeeded.
Promise.any() returns a promise that resolves as soon as any one of the input promises resolves successfully. It ignores rejections unless all promises reject, in which case it throws an AggregateError containing all the rejection reasons. This is the optimistic counterpart to Promise.race() - race settles on the first promise to settle (resolve or reject), while any settles on the first promise to resolve. Use cases include fetching from multiple CDN mirrors and using whichever responds first, or trying multiple authentication strategies.
In practice, these combinators are often combined with async/await for clean, readable code. Destructuring the results of Promise.all() is a common pattern: const [users, posts, comments] = await Promise.all([fetchUsers(), fetchPosts(), fetchComments()]). Understanding these combinators and their failure modes is essential for writing performant and resilient asynchronous JavaScript.
function fetchResource(name, delay, shouldFail = false) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldFail) reject(new Error(`${name} failed`));
else resolve({ name, data: `${name} data` });
}, delay);
});
}
// All succeed - get all results
async function loadDashboard() {
try {
const [users, posts, stats] = await Promise.all([
fetchResource('Users', 100),
fetchResource('Posts', 200),
fetchResource('Stats', 150)
]);
console.log(users.name, posts.name, stats.name);
} catch (err) {
console.log('Error:', err.message);
}
}
loadDashboard();
// One fails - entire Promise.all rejects
async function loadWithFailure() {
try {
const results = await Promise.all([
fetchResource('API-1', 100),
fetchResource('API-2', 50, true), // this fails
fetchResource('API-3', 200)
]);
console.log('This never runs');
} catch (err) {
console.log('Caught:', err.message);
}
}
setTimeout(() => loadWithFailure(), 300);
When all promises resolve, Promise.all returns an array of results in order. When API-2 fails, Promise.all immediately rejects without waiting for API-1 or API-3. The other promises still run in the background, but their results are discarded.
function fetchWithDelay(source, delay) {
return new Promise(resolve => {
setTimeout(() => resolve(`Data from ${source}`), delay);
});
}
function timeout(ms) {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error('Request timed out')), ms);
});
}
// Racing: fastest response wins
async function fetchFastest() {
const result = await Promise.race([
fetchWithDelay('CDN-US', 200),
fetchWithDelay('CDN-EU', 100), // fastest
fetchWithDelay('CDN-Asia', 300)
]);
console.log('Winner:', result);
}
fetchFastest();
// Timeout pattern: race fetch against a timer
async function fetchWithTimeout() {
try {
const result = await Promise.race([
fetchWithDelay('Slow API', 5000), // slow
timeout(200) // timeout after 200ms
]);
console.log('Got:', result);
} catch (err) {
console.log(err.message);
}
}
setTimeout(() => fetchWithTimeout(), 400);
In the first race, CDN-EU resolves fastest at 100ms. In the timeout pattern, the slow API takes 5000ms but the timeout rejects at 200ms. Since the timeout rejects first, Promise.race rejects with the timeout error. This is a common pattern for adding timeouts to any promise-based operation.
function sendNotification(channel, shouldFail = false) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldFail) {
reject(new Error(`${channel} delivery failed`));
} else {
resolve({ channel, status: 'delivered' });
}
}, 100);
});
}
async function notifyUser() {
const results = await Promise.allSettled([
sendNotification('Email'),
sendNotification('SMS', true), // fails
sendNotification('Push'),
sendNotification('Slack', true) // fails
]);
// Categorize results
const succeeded = results.filter(r => r.status === 'fulfilled');
const failed = results.filter(r => r.status === 'rejected');
console.log(`Delivered: ${succeeded.length}/${results.length}`);
succeeded.forEach(r => console.log(` OK: ${r.value.channel}`));
failed.forEach(r => console.log(` FAIL: ${r.reason.message}`));
}
notifyUser();
Promise.allSettled waits for every promise and reports each outcome individually. No short-circuiting on failure. Each result object has status ('fulfilled' or 'rejected') and either value or reason. This is perfect when partial failure is acceptable.
function tryMirror(mirror, delay, shouldFail = false) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldFail) {
reject(new Error(`${mirror} unavailable`));
} else {
resolve(`Downloaded from ${mirror}`);
}
}, delay);
});
}
// First successful resolution wins
async function downloadPackage() {
try {
const result = await Promise.any([
tryMirror('Mirror-1', 300, true), // fails
tryMirror('Mirror-2', 200), // succeeds at 200ms
tryMirror('Mirror-3', 100, true) // fails
]);
console.log('Success:', result);
} catch (err) {
console.log('All failed');
}
}
downloadPackage();
// All fail - AggregateError
async function allFail() {
try {
await Promise.any([
tryMirror('A', 100, true),
tryMirror('B', 100, true),
tryMirror('C', 100, true)
]);
} catch (err) {
console.log('Error type:', err.constructor.name);
console.log('Errors:', err.errors.map(e => e.message));
}
}
setTimeout(() => allFail(), 400);
Promise.any ignores rejections and resolves with the first fulfilled promise. Mirror-1 and Mirror-3 fail, but Mirror-2 succeeds at 200ms, so that becomes the result. When ALL promises reject, Promise.any throws an AggregateError containing every rejection reason.
Promise.all, Promise.race, Promise.allSettled, Promise.any, parallel promises, AggregateError