When to Use Promise.all, Promise.allSettled, Promise.race, and Promise.any
Promise combinators are not four interchangeable ways to make async code "faster." They answer different questions. Use Promise.all when every task must succeed, Promise.allSettled when every result matters even after failures, Promise.race when the first settled Promise decides the outcome, and Promise.any when the first successful Promise is enough.
That is the whole map. The rest is learning not to lie to yourself about which outcome your program actually needs.
Contents
- The quick decision table
- The naive approach waits too much
- Use Promise.all when all tasks must succeed
- Use Promise.allSettled when every outcome matters
- Use Promise.race when the first settlement wins
- Use Promise.any when the first success wins
- Common traps
- Key Takeaways
The quick decision table
Start with the result shape:
| Need | Use | Fulfilled value | Rejects when |
|---|---|---|---|
| All tasks must succeed | Promise.all | Array of fulfilled values | Any input rejects |
| Every task must report its outcome | Promise.allSettled | Array of status objects | Never rejects because of input Promises |
| First task to settle decides | Promise.race | First fulfilled value | First settled Promise rejects |
| First success is enough | Promise.any | First fulfilled value | All inputs reject |
That table is more useful than memorizing examples. Concurrency bugs usually come from choosing the method by vibes instead of by failure behavior.
The naive approach waits too much
The beginner pattern is to put await in front of every async call:
async function loadDashboard() {
const user = await fetchUser();
const notifications = await fetchNotifications();
const activity = await fetchActivity();
return { user, notifications, activity };
}
That is correct only if each step depends on the previous step. If the three requests are independent, this code creates unnecessary waiting:
fetchUser starts
fetchUser finishes
fetchNotifications starts
fetchNotifications finishes
fetchActivity starts
fetchActivity finishes
The fix is not "sprinkle Promise.all everywhere." The fix is to start independent work together, then choose the combinator that matches your failure policy.
async function loadDashboard() {
const [user, notifications, activity] = await Promise.all([
fetchUser(),
fetchNotifications(),
fetchActivity(),
]);
return { user, notifications, activity };
}
Now the requests overlap. The total wait is roughly the slowest request, not the sum of all three.
Use Promise.all when all tasks must succeed
Promise.all is the right tool when one failure makes the whole operation unusable.
async function loadAccountPage(accountId: string) {
const [account, plan, permissions] = await Promise.all([
fetchAccount(accountId),
fetchPlan(accountId),
fetchPermissions(accountId),
]);
return { account, plan, permissions };
}
If fetchPermissions fails, the page probably should not pretend it has enough data. Promise.all rejects as soon as one input rejects.
The order of the returned values matches the order of the input array, not the order the Promises finished:
const [profile, settings] = await Promise.all([
fetchProfile(), // result goes into profile
fetchSettings(), // result goes into settings
]);
That predictability is useful. The tradeoff is blunt failure behavior. One rejection rejects the whole combined Promise.
const result = await Promise.all([
Promise.resolve("profile"),
Promise.reject(new Error("settings failed")),
Promise.resolve("activity"),
]);
// The await throws. You do not receive ["profile", ?, "activity"].
Use it when partial data is not acceptable.
Use Promise.allSettled when every outcome matters
Promise.allSettled waits for every input Promise to settle. It does not reject just because an input rejected. Instead, each item becomes a status object.
const results = await Promise.allSettled([
fetchProfile(),
fetchRecommendations(),
fetchActivity(),
]);
for (const result of results) {
if (result.status === "fulfilled") {
console.log("Loaded:", result.value);
} else {
console.log("Failed:", result.reason);
}
}
This is the right shape for independent optional work:
async function loadHomePage() {
const [profile, recommendations, activity] = await Promise.allSettled([
fetchProfile(),
fetchRecommendations(),
fetchActivity(),
]);
return {
profile:
profile.status === "fulfilled" ? profile.value : null,
recommendations:
recommendations.status === "fulfilled" ? recommendations.value : [],
activity:
activity.status === "fulfilled" ? activity.value : [],
};
}
That code is noisier than Promise.all, and that is the point. Partial failure has to be handled deliberately. Otherwise you get the classic dashboard where one flaky sidebar takes down the entire page. Elegant, in the same way a loose stair is elegant.
Use Promise.allSettled for batch jobs, optional panels, upload queues, validation across several independent fields, and reporting where failures are data.
Use Promise.race when the first settlement wins
Promise.race resolves or rejects with the first Promise that settles. Fulfillment and rejection both count.
const first = await Promise.race([
fetchFromPrimaryRegion(),
fetchFromBackupRegion(),
]);
If the primary request fulfills first, first is its value. If the backup request rejects first, the race rejects even if the primary request would have succeeded 20 milliseconds later.
That rejection behavior is why Promise.race is sharp. It answers "what settled first?" not "what succeeded first?"
A common timeout pattern uses Promise.race:
function timeout(ms: number) {
return new Promise<never>((_, reject) => {
setTimeout(() => {
reject(new Error(`Timed out after ${ms}ms`));
}, ms);
});
}
async function loadWithTimeout() {
return Promise.race([
fetch("/api/report"),
timeout(5000),
]);
}
This stops waiting after 5 seconds. It does not cancel the fetch request. The browser request can keep running in the background.
If you need real request cancellation, use AbortController with fetch instead of only racing against a timer.
Use Promise.any when the first success wins
Promise.any fulfills with the first fulfilled value. Rejections are ignored unless every input rejects.
const product = await Promise.any([
fetchFromCdn("us-east"),
fetchFromCdn("us-west"),
fetchFromCdn("eu-central"),
]);
This is useful when you have redundant sources and only need one successful answer. A rejection from the first source should not doom the operation if another source succeeds.
If every input rejects, Promise.any rejects with an AggregateError:
try {
const value = await Promise.any([
Promise.reject(new Error("primary failed")),
Promise.reject(new Error("backup failed")),
]);
console.log(value);
} catch (error) {
if (error instanceof AggregateError) {
console.log(error.errors);
}
}
The difference from Promise.race is not subtle:
| Inputs | Promise.race | Promise.any |
|---|---|---|
| Fast rejection, slow success | Rejects | Fulfills with the slow success |
| Fast success, slow rejection | Fulfills | Fulfills |
| All reject | Rejects with first rejection | Rejects with AggregateError |
Use Promise.any when failure is tolerable as long as one option works.
Common traps
Promise.all does not start Promises. Calling the async functions starts the work.
const tasks = [
fetchUser(),
fetchSettings(),
];
await Promise.all(tasks);
In that example, fetchUser() and fetchSettings() are called before Promise.all receives the array. Promise.all only waits for the Promises it is given.
Do not pass function references when you meant to start work:
await Promise.all([
fetchUser,
fetchSettings,
]);
That resolves immediately with the functions themselves. Slightly embarrassing. Also very normal.
Another trap: none of these methods limit concurrency. If you pass 500 fetches to Promise.all, you asked for 500 operations to begin.
await Promise.all(userIds.map((id) => fetchUser(id)));
That might be fine for 5 users. It is probably rude at 5,000. Use a queue or batch the work if the system on the other end matters, which it usually does.
Finally, remember that fetch() does not reject on HTTP status codes like 404 or 500. A failed HTTP response still fulfills the fetch Promise. Check response.ok yourself or read why fetch does not throw on 404 errors.
Key Takeaways
- Use
Promise.allwhen partial success is useless and one failure should reject the whole operation. - Use
Promise.allSettledwhen failures are part of the result you need to inspect. - Use
Promise.racewhen the first settled Promise wins, including fast rejections. - Use
Promise.anywhen the first successful result wins and early failures should be ignored. - Promise combinators coordinate work. They do not cancel work, validate HTTP status codes, or limit concurrency for you.