handling partial failures without aborting the whole operation
Promise.allSettled waits for every given promise, regardless of whether individual ones fulfill or reject, and delivers its own status per result. For batch requests where a single failure should not wreck the entire operation, that is the right foundation instead of the fail-fast behavior of Promise.all.
Table of Contents
- 1. Why Promise.allSettled needs a different error model
- 2. Syntax and return shape in detail
- 3. Handling batch requests with partial failures robustly
- 4. Filtering and aggregating results by status
- 5. Retry strategies for failed entries
- 6. Combining with a timeout per individual promise
- 7. Communicating user feedback and partial success
- 8. When Promise.allSettled is not worth it
- 9. Promise.allSettled vs all vs any vs race
- 10. Summary
- 11. FAQ
1. Why Promise.allSettled needs a different error model
Promise.allSettled was introduced because Promise.all has the wrong error model for many real world use cases. Promise.all rejects immediately as soon as any of the given promises rejects, regardless of whether the rest have already completed successfully or are about to. In a batch of twenty independent API calls, a single failure means the results of the nineteen successful calls are completely lost, because Promise.all never returns them.
Promise.allSettled solves that by never rejecting itself. Instead it waits until every given promise has either fulfilled or rejected, and delivers a result object with the actual status for every element. That shifts the decision of how to handle individual failures entirely to the caller, instead of implicitly preempting it through the fail-fast behavior of Promise.all. For scenarios like bulk imports, parallel health checks, or loading several independent widgets on a page, this is almost always the more appropriate behavior.
2. Syntax and return shape in detail
Promise.allSettled(promises) accepts an iterable of promises and itself returns a promise that always fulfills, never rejects, once all given promises have settled. The resolved result is an array of objects, in the same order as the input. Every object has a status field that is either 'fulfilled' or 'rejected'.
With status: 'fulfilled', the object additionally contains a value field with the resolved result. With status: 'rejected', it instead contains a reason field with the error or rejection cause. This structure makes Promise.allSettled a discriminated union type in TypeScript terms: the status field reliably determines whether value or reason should be read, without a try/catch per element.
const results = await Promise.allSettled([
fetch('/api/users').then((r) => r.json()),
fetch('/api/orders').then((r) => r.json()),
fetch('/api/broken-endpoint').then((r) => r.json()),
]);
console.log(results);
// [
// { status: 'fulfilled', value: [...users] },
// { status: 'fulfilled', value: [...orders] },
// { status: 'rejected', reason: TypeError: Failed to fetch }
// ]
3. Handling batch requests with partial failures robustly
The classic use case for Promise.allSettled is a bulk import or bulk export where every individual record is processed independently of the rest. Importing a thousand products into a shop system, for instance, should not abort entirely just because a single record contains an invalid field. With Promise.allSettled, all thousand processing attempts run in parallel, and at the end it is clear which ones succeeded and which failed for which reason.
This approach also fundamentally changes the monitoring of such batch operations. Instead of a binary success or failure state, Promise.allSettled delivers a granular success rate that translates directly into a dashboard: 987 out of 1000 succeeded, 13 failed each with a specific error message. That granularity is practically impossible to achieve with Promise.all, because there the first failure already discards all further information.
async function importProducts(products) {
const results = await Promise.allSettled(
products.map((product) => importSingleProduct(product))
);
const succeeded = results.filter((r) => r.status === 'fulfilled').length;
const failed = results.filter((r) => r.status === 'rejected').length;
console.log(`Import finished: ${succeeded} succeeded, ${failed} failed`);
return results;
}
4. Filtering and aggregating results by status
After resolving Promise.allSettled, the most common follow up operation is separating successful and failed results into two distinct lists. That is typically done with two filter calls on the status field, followed by a map that extracts either value or reason. This separation is where the actual business logic decides what happens with successful entries versus failed ones.
A common mistake when using Promise.allSettled is forgetting to filter by status and instead accessing value directly, which returns undefined for a rejected entry instead of an error. TypeScript helps here directly, because the discriminated union type does not even allow accessing value without a preceding status check. In plain JavaScript, a small utility function that encapsulates this distinction is worthwhile.
function partitionSettled(results) {
const fulfilled = [];
const rejected = [];
for (const result of results) {
if (result.status === 'fulfilled') {
fulfilled.push(result.value);
} else {
rejected.push(result.reason);
}
}
return { fulfilled, rejected };
}
const { fulfilled, rejected } = partitionSettled(results);
console.log(`${fulfilled.length} values, ${rejected.length} errors`);
5. Retry strategies for failed entries
Because Promise.allSettled returns every failed entry including its original index, a retry strategy can be built directly on top of it. Instead of running the entire batch again when a part fails, only the subset of failed elements is reprocessed with another Promise.allSettled call. That significantly reduces unnecessary load on external systems, especially when the original batch contained several thousand elements and only a small minority actually failed.
A robust retry implementation limits the number of attempts and waits between attempts with exponential backoff, to absorb transient failures such as brief network issues or rate limiting. Since Promise.allSettled itself never rejects, this loop can be written without an additional try/catch around the entire batch, every single retry pass delivers a complete result array again.
async function importWithRetry(items, maxAttempts = 3) {
let pending = items.map((item, index) => ({ item, index }));
const finalResults = new Array(items.length);
for (let attempt = 1; attempt <= maxAttempts && pending.length > 0; attempt++) {
const results = await Promise.allSettled(
pending.map(({ item }) => importSingleProduct(item))
);
const stillFailing = [];
results.forEach((result, i) => {
const { index } = pending[i];
if (result.status === 'fulfilled') {
finalResults[index] = result;
} else {
stillFailing.push(pending[i]);
}
});
pending = stillFailing;
if (pending.length > 0) {
await new Promise((r) => setTimeout(r, 500 * attempt)); // backoff
}
}
return finalResults;
}
6. Combining with a timeout per individual promise
An important detail with Promise.allSettled: a hanging request that never resolves blocks the entire result, because Promise.allSettled waits for every single entry. Without a per-promise timeout, a single slow or hanging request can delay the entire batch operation indefinitely, even if all other requests finished long ago. That is why it is common to attach a timeout to every individual promise before passing it to Promise.allSettled.
Combined with AbortSignal.timeout(), this can be built directly into the creation of every single promise, so a hanging request automatically rejects after a defined deadline and flows into the overall result of Promise.allSettled, instead of holding up the entire batch. This combination is almost always sensible in practice, as soon as a batch consists of network requests whose runtime is not guaranteed.
async function fetchWithTimeout(url, timeoutMs = 5000) {
const response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
return response.json();
}
const results = await Promise.allSettled(
endpoints.map((url) => fetchWithTimeout(url, 3000))
);
// A hanging endpoint rejects with TimeoutError after 3s instead of blocking forever
7. Communicating user feedback and partial success
In UI contexts, Promise.allSettled allows for far better user communication than a binary success-failure model. A form uploading several files at once can, with Promise.allSettled, precisely show which files succeeded and which failed for which reason, instead of reporting the entire upload process as failed on the first failed file and giving the user no information about the remaining files.
This granular feedback noticeably reduces support requests, because users can directly see which part of their action succeeded and which needs to be retried. Instead of upload failed, the interface shows eight of ten files uploaded, two files exceed the size limit, which names the cause directly and gives the user a concrete course of action.
8. When Promise.allSettled is not worth it
Not every use case benefits from Promise.allSettled. When all operations depend on each other and a failure in one operation renders the remaining results worthless anyway, for example when loading several parts of a single connected data structure in parallel, the fail-fast behavior of Promise.all is actually the right choice. Promise.allSettled would in that case wait needlessly long for results that ultimately have to be discarded regardless.
A second point concerns error handling itself: Promise.allSettled shifts the responsibility of inspecting every reason onto the caller. Anyone who does not take on that responsibility and silently ignores errors loses exactly the information that Promise.all would have made visible automatically through its rejection. Promise.allSettled is a tool for explicit, deliberate error handling, not for hiding errors.
9. Promise.allSettled vs all vs any vs race
The four static promise combinators differ fundamentally in their error behavior and in what they reveal about the state of all participating promises. The following table compares them.
| Method | Resolves when | Rejects when | Typical use |
|---|---|---|---|
Promise.allSettled |
All settled (either way) | Never | Batch requests with allowed partial failures |
Promise.all |
All fulfilled | First failure | Interdependent operations |
Promise.any |
First success | All failed | First successful source among several |
Promise.race |
First settlement (success or failure) | First settlement is a failure | Timeout race against an operation |
Choosing the right method depends directly on whether partial failures are acceptable (allSettled), whether all results are strictly required (all), whether a single success is enough (any), or whether only the fastest result counts (race).
Mironsoft
Robust batch processing and error handling in JavaScript
A single failure crashes your entire batch?
We build batch processing and bulk imports with Promise.allSettled, granular failure reporting and retry strategies for failed entries.
Audit
Identifying fail-fast spots that could actually tolerate partial failures
Refactoring
Migration to Promise.allSettled with retry and timeout handling
Monitoring
Granular success rates for batch operations in a dashboard
10. Summary
Promise.allSettled is the right choice as soon as individual failures in a batch of operations are expected and acceptable. Unlike Promise.all, it never rejects itself but instead delivers a result object with status, value, or reason for every given promise, allowing successful and failed entries to be cleanly separated and processed further independently.
In practice, it is worth combining this with a timeout per individual promise, a retry strategy for failed entries, and granular user feedback about partial success. Where all operations strictly depend on each other, the fail-fast behavior of Promise.all remains the more appropriate choice, but for independent batch operations Promise.allSettled is almost always the more robust foundation.
Promise.allSettled in practice — the essentials at a glance
Return shape
Array of objects with status: 'fulfilled' or 'rejected', each with value or reason.
No fail-fast
Promise.allSettled never rejects itself, it always waits for every given promise.
Targeted retry
Reprocess only failed entries instead of repeating the entire batch.
Timeout per promise
AbortSignal.timeout() per individual promise prevents a hanging request from blocking the whole batch.