TidyURLs
Go to Tools
BLOG
The TidyURLsJournal

Practical guides, tutorials, and insights about URLs, SEO, web development, and online marketing. Learn, troubleshoot, and get the most of the web.

Back to the Journal
TidyURLs Journal

Promise.all vs Promise.allSettled: Handling Partial API Failures

Practical insights, clear explanations, and useful ideas for understanding the web.

October 26, 20266 min read5 topics
Promise.all vs Promise.allSettled: Handling Partial API Failures
Explore the details
#JavaScript#Promises#Node.js#Async Programming#Error Handling
October 26, 20266 min read

When an API request fails, should your JavaScript code reject the entire operation or keep the results from requests that succeeded? Promise.all and Promise.allSettled make different choices. This guide compares their rejection behavior, shows what each returns, and walks through a runnable example where one request fails but the others continue.

Key Takeaways

  • Promise.all fulfills with values in input order when every input fulfills. It rejects when an input rejects.
  • Promise.allSettled waits for every input to settle and returns an outcome for each one, in input order.
  • Both combinators observe promises; neither starts every kind of work nor cancels work after a failure.
  • Use Promise.all when every result is needed to proceed. Use Promise.allSettled when independent results are useful even if some operations fail.

How their failure behavior differs

Promise.all is often called fail-fast: its returned promise rejects when an input rejects. It does not wait for the other inputs to finish before rejecting. If all inputs fulfill, the result is an array of their values.

Promise.allSettled takes a different approach. Its returned promise fulfills after all inputs settle—meaning each has either fulfilled or rejected. It returns an array of outcome objects. A fulfilled outcome has the shape { status: "fulfilled", value: ... }; a rejected outcome has the shape { status: "rejected", reason: ... }. The rejection reason can be any JavaScript value, though errors are commonly Error objects.

Both methods preserve the order of the input values in their result arrays. That order is not necessarily the order in which the operations finish.

“Fail-fast” does not mean “cancel the rest”

A rejection from Promise.all does not cancel the other promises. Their underlying operations keep running unless something else stops them. For example, an HTTP request may still complete and cause side effects after the aggregate promise has rejected.

Also, calling asynchronous functions while building the input array usually starts their work before Promise.all receives the array. Neither combinator is a task scheduler: it does not make synchronous CPU-heavy work run in parallel, and it does not impose a concurrency limit.

Runnable example: one failure, two outcomes

Save this as promise-comparison.js and run it with Node.js:

node promise-comparison.js

The example starts three operations together. The orders operation rejects first. Promise.all reports that rejection, while the recommendations operation continues. The subsequent Promise.allSettled call shows the final outcome of every operation.

const delay = (milliseconds) =>
  new Promise((resolve) => setTimeout(resolve, milliseconds));

async function main() {
  const requests = [
    delay(100).then(() => ({ profile: "Ada" })),
    delay(200).then(() => {
      throw new Error("Orders service unavailable");
    }),
    delay(400).then(() => ({ recommendations: ["book", "map"] })),
  ];

  try {
    const values = await Promise.all(requests);
    console.log("All requests succeeded:", values);
  } catch (error) {
    console.log("Promise.all rejected:", error.message);
  }

  const outcomes = await Promise.allSettled(requests);
  const summary = outcomes.map((outcome, index) => {
    if (outcome.status === "fulfilled") {
      return { index, status: outcome.status, value: outcome.value };
    }

    return {
      index,
      status: outcome.status,
      reason: outcome.reason.message,
    };
  });

  console.log("All outcomes:");
  console.log(JSON.stringify(summary, null, 2));
}

main().catch((error) => {
  console.error("Unexpected example error:", error);
  process.exitCode = 1;
});

The output is:

Promise.all rejected: Orders service unavailable
All outcomes:
[
  {
    "index": 0,
    "status": "fulfilled",
    "value": {
      "profile": "Ada"
    }
  },
  {
    "index": 1,
    "status": "rejected",
    "reason": "Orders service unavailable"
  },
  {
    "index": 2,
    "status": "fulfilled",
    "value": {
      "recommendations": [
        "book",
        "map"
      ]
    }
  }
]

The order of the results follows the order of requests, even though the operations finish at different times. The second call to Promise.allSettled reuses the same promises; it does not start the requests again.

Choosing the right combinator

Use Promise.all when success depends on every operation succeeding. For example, if a page cannot be rendered without both the account and permissions, rejecting the combined operation may be the clearest behavior.

Use Promise.allSettled when results are independent and partial data is still useful. A dashboard might show available service statuses even when one status check fails. The caller can then decide what to display for each outcome.

NeedChooseWhat the caller receives
Every operation must succeedPromise.allValues, or a rejection when an input rejects
Keep track of every success and failurePromise.allSettledOne fulfilled or rejected outcome per input

Neither method automatically retries failures or limits how many operations run at once. Those are separate policies that should be implemented where appropriate.

Turning settled outcomes into partial data

A common pattern is to map settled results back to named operations. This keeps successful data while making failures available for logging, fallback UI, or metrics:

const names = ["profile", "orders", "recommendations"];
const outcomes = await Promise.allSettled(requests);

const resultByName = Object.fromEntries(
  outcomes.map((outcome, index) => [names[index], outcome])
);

if (resultByName.profile.status === "fulfilled") {
  console.log("Profile:", resultByName.profile.value);
} else {
  console.error("Profile failed:", resultByName.profile.reason);
}

if (resultByName.orders.status === "fulfilled") {
  console.log("Orders:", resultByName.orders.value);
} else {
  console.error("Orders failed:", resultByName.orders.reason);
}

This snippet assumes requests and names use the same order, as in the previous example. For larger applications, consider pairing each operation with its name directly so that later edits cannot accidentally misalign the two arrays.

What if you need to stop the remaining work?

Neither Promise.all nor Promise.allSettled provides general cancellation. If you need cancellation, use a mechanism supported by the underlying operations. For example, APIs such as fetch can use an AbortController signal, but aborting one request does not automatically abort other work. You must pass and handle the relevant signals yourself.

Cancellation is not the same as rollback: a request may already have reached a server or caused a side effect before it is aborted. Use server-side safeguards when an operation must be safe to repeat or stop.

Common Mistakes

  • Assuming Promise.all stops other requests. It rejects the aggregate promise; it does not cancel the inputs.
  • Assuming Promise.allSettled rejects when an input rejects. A rejected input is represented as an outcome object; the aggregate fulfills after all inputs settle.
  • Confusing input order with completion order. Both result arrays follow input order.
  • Using allSettled but ignoring rejected outcomes. Check each status before reading value; rejected outcomes have reason instead.
  • Treating these methods as concurrency controls. If you need a limit on simultaneous operations, use a separate concurrency-control approach.

Best Practices

  • Choose based on whether the overall task requires every operation to succeed, not just on which method seems more tolerant of errors.
  • For partial results, handle each rejected outcome explicitly and provide an appropriate fallback.
  • Preserve operation names alongside outcomes so failures can be reported clearly.
  • If cancellation or time limits are required, implement them at the operation level and verify that the underlying API supports them.
  • Remember that promises created before calling either combinator may already be running.

Conclusion

Use Promise.all for work that succeeds only when every input fulfills. Use Promise.allSettled when you need to inspect every result and keep useful partial data. Neither cancels remaining operations, so cancellation and concurrency limits require separate handling.

References