Using Promise.allSettled for Reliable JavaScript APIs

Learn how Promise.allSettled enables partial-success patterns in JavaScript. Understand how it differs from Promise.all, handle mixed outcomes gracefully, and build resilient API calls that never fail silently.

JavaScriptintermediate
11 min read

Promise.all is great when every operation must succeed. But real applications deal with partial failure constantly — a batch of API calls where some succeed and some fail, enriching data from optional sources, or retrying only the failed subset of a batch. Promise.allSettled is designed precisely for these scenarios. It never rejects and gives you a complete picture of every outcome.

The Core Problem Promise.allSettled Solves

With Promise.all, one failure rejects the entire result:

javascriptjavascript
// Building a dashboard from three data sources
const [users, metrics, featureFlags] = await Promise.all([
  fetchUsers(),         // Critical
  fetchMetrics(),       // Critical
  fetchFeatureFlags(),  // Optional — outage would kill the whole dashboard
]);

If fetchFeatureFlags() has a temporary outage, the entire dashboard breaks, even though users and metrics loaded fine. Promise.allSettled lets you continue with what you have.

How Promise.allSettled Works

Promise.allSettled waits for every Promise to settle — fulfilled OR rejected — and resolves with an array of outcome objects:

javascriptjavascript
const results = await Promise.allSettled([
  fetchUsers(),
  fetchMetrics(),
  fetchFeatureFlags(),
]);
 
// results is always an array with the same length as the input:
// [
//   { status: "fulfilled", value: [...users] },
//   { status: "fulfilled", value: {...metrics} },
//   { status: "rejected",  reason: Error("Feature flags service unavailable") },
// ]

Each element is either:

  • { status: "fulfilled", value: <result> } — the Promise fulfilled
  • { status: "rejected", reason: <error> } — the Promise rejected

The outer Promise returned by Promise.allSettled never rejects. You always get the array.

Extracting Results Cleanly

javascriptjavascript
const results = await Promise.allSettled([
  fetchUsers(),
  fetchMetrics(),
  fetchFeatureFlags(),
]);
 
// Destructure with fallbacks
const [usersResult, metricsResult, flagsResult] = results;
 
const users = usersResult.status === "fulfilled"
  ? usersResult.value
  : [];
 
const metrics = metricsResult.status === "fulfilled"
  ? metricsResult.value
  : defaultMetrics;
 
const flags = flagsResult.status === "fulfilled"
  ? flagsResult.value
  : {};
 
if (flagsResult.status === "rejected") {
  console.warn("Feature flags unavailable, using defaults:", flagsResult.reason.message);
}
 
renderDashboard({ users, metrics, flags });

Helper: Partition Fulfilled and Rejected

A reusable helper for batch operations makes the pattern concise:

javascriptjavascript
// Helper: Split allSettled results into successes and failures
function partitionSettled(results) {
  return results.reduce(
    (acc, result) => {
      if (result.status === "fulfilled") {
        acc.fulfilled.push(result.value);
      } else {
        acc.rejected.push(result.reason);
      }
      return acc;
    },
    { fulfilled: [], rejected: [] }
  );
}
 
// Usage
async function batchFetchUsers(ids) {
  const results = await Promise.allSettled(
    ids.map((id) => fetchUser(id))
  );
 
  const { fulfilled: users, rejected: errors } = partitionSettled(results);
 
  if (errors.length > 0) {
    console.warn(`${errors.length}/${ids.length} user fetches failed`);
    errors.forEach((e) => console.warn(" -", e.message));
  }
 
  return users;
}

Practical Pattern: Data Enrichment

A common real-world use case is enriching a list of core objects with optional data from additional sources. Some enrichments may fail without blocking the core flow:

javascriptjavascript
async function enrichArticles(articles) {
  const enrichmentResults = await Promise.allSettled(
    articles.map(async (article) => {
      const [views, comments] = await Promise.all([
        fetchViewCount(article.id),
        fetchCommentCount(article.id),
      ]);
      return { id: article.id, views, comments };
    })
  );
 
  return articles.map((article, i) => {
    const result = enrichmentResults[i];
    if (result.status === "fulfilled") {
      return { ...article, ...result.value };
    }
    // Enrichment failed: return the article as-is
    return { ...article, views: null, comments: null };
  });
}

Practical Pattern: Retry Only Failures

Promise.allSettled makes it easy to identify and retry failed operations:

javascriptjavascript
async function fetchWithRetrySubset(items, fetcher, maxRetries = 2) {
  let pending = items;
  const succeeded = [];
  let attempt = 0;
 
  while (pending.length > 0 && attempt < maxRetries) {
    const results = await Promise.allSettled(pending.map(fetcher));
 
    const nextPending = [];
    for (let i = 0; i < results.length; i++) {
      if (results[i].status === "fulfilled") {
        succeeded.push(results[i].value);
      } else {
        nextPending.push(pending[i]); // Keep for retry
        console.warn(`Retrying item ${pending[i].id}:`, results[i].reason.message);
      }
    }
 
    pending = nextPending;
    attempt++;
 
    if (pending.length > 0 && attempt < maxRetries) {
      await new Promise((r) => setTimeout(r, 500 * attempt)); // Backoff
    }
  }
 
  return {
    succeeded,
    failed: pending, // Items that failed all retries
  };
}

allSettled vs all: Decision Table

ScenarioUse
Every result is required (payment processing steps)Promise.all
Dashboard with optional data sectionsPromise.allSettled
Batch import where partial success is acceptablePromise.allSettled
Parallel prerequisite fetches (all required before next step)Promise.all
Logging/analytics fire-and-forget (failures acceptable)Promise.allSettled
Enriching objects with optional metadataPromise.allSettled

Combining allSettled With Typed Results

Using TypeScript or JSDoc, you can type the results for strict handling:

javascriptjavascript
/**
 * @template T
 * @param {Promise<T>[]} promises
 * @returns {Promise<{value: T | null, error: Error | null}[]>}
 */
async function settleAll(promises) {
  const results = await Promise.allSettled(promises);
  return results.map((r) => ({
    value: r.status === "fulfilled" ? r.value : null,
    error: r.status === "rejected" ? r.reason : null,
  }));
}
 
// Normalized result:
// [
//   { value: {...user}, error: null },
//   { value: null, error: Error("Not found") },
// ]

The Difference From Promise.all in Error Handling

javascriptjavascript
// Promise.all: first rejection shortcuts everything
try {
  const results = await Promise.all([p1, p2, p3]);
  // Either all three values, or catch fires
} catch (err) {
  // You only know WHICH one failed if the Error has identifying info
  // The other Promises already ran but their results are lost
}
 
// Promise.allSettled: always get all outcomes
const results = await Promise.allSettled([p1, p2, p3]);
// Always 3 results, each labeled fulfilled or rejected
// No need for try/catch — the outer Promise never rejects

For more on Promise.all and the complete concurrency picture, see how to use Promise.all.

Rune AI

Rune AI

Key Insights

  • Promise.allSettled never rejects: The outer Promise always fulfills with an array of outcome objects, making try/catch unnecessary at the allSettled level
  • Every result is a status object: { status: "fulfilled", value } or { status: "rejected", reason } — always check status before accessing value
  • Order is preserved: results[i] always corresponds to promises[i], enabling safe zip-back of results to input items
  • Use it for optional/partial data: When some operations failing should not block the overall flow — dashboard sections, enrichment, analytics — allSettled is the right choice over Promise.all
  • Log rejected results explicitly: Partial failures are easy to miss — always log or track which operations in the batch failed, even if the overall function continues successfully
RunePowered by Rune AI

Frequently Asked Questions

Does Promise.allSettled ever reject?

No. The Promise returned by `Promise.allSettled` always fulfills with an array of outcome objects. Individual Promises inside it can reject, but those rejections are captured as `{ status: "rejected", reason: ... }` entries in the array rather than propagating outward.

When was Promise.allSettled introduced?

`Promise.allSettled` was introduced in ES2020 (ES11). It is supported in all modern browsers and Node.js 12.9+. For older environments, it can be polyfilled or replicated with `.then(value => ({ status: "fulfilled", value }), reason => ({ status: "rejected", reason }))` on each Promise.

Can I use Promise.allSettled as a replacement for Promise.all everywhere?

Technically yes, but it is not a good practice. `Promise.allSettled` signals to readers that partial failure is acceptable. Using it where all results are actually required hides the fact that some data might be missing silently. Use `Promise.all` when every result is required to communicate that intent clearly.

How do I know which input Promise corresponds to which result?

The results array always preserves the same order as the input array. `results[0]` corresponds to the first Promise, `results[1]` to the second, and so on. This ordering guarantee lets you safely zip results back with their input items.

Can I combine Promise.allSettled with Promise.race inside it?

Yes. The Promises inside `Promise.allSettled` can themselves be the result of `Promise.race`, `Promise.any`, or any other composition. Each input is just a Promise regardless of how it was created.

Conclusion

Promise.allSettled is the right tool when partial success is acceptable or expected. It guarantees you always see the full picture — every outcome for every input — without the fail-fast behavior of Promise.all. Use it for enrichment, batch processing, and optional data sources where the core flow should not break because of one failure. Always handle rejections in the settled results explicitly rather than silently ignoring them.