DEV Community

Cover image for Does Promise.all() Cancel Other Promises When One Rejects?

Does Promise.all() Cancel Other Promises When One Rejects?

Shubhra Pokhariya on November 10, 2026

When Promise.all() rejects, the promise it returns settles with the first rejection reason, and that is all that changes. Nothing is cancelled. The...
Collapse
 
webdeveloperhyper profile image
Web Developer Hyper •

Very detailed explanation of promises and their related functions. Nice work! 👍

Collapse
 
shubhradev profile image
Shubhra Pokhariya •

Thank you so much! Glad you found the explanation useful. 😊

Collapse
 
delayrun profile image
delay.run •

The settings page example is spot on. "Save failed" when two of the three saves went through is exactly how people end up with duplicate records and no idea why. For the retry, I've had good results sending only the parts that failed, using Promise.allSettled results, instead of resending the whole group.

Collapse
 
shubhradev profile image
Shubhra Pokhariya •

Thanks! That's the approach I tested in the third scenario. It retries only the failed write, and each write ends up applied once in that run.

The catch is that a rejection doesn't always mean the write wasn't applied. A validation error usually does. A timeout doesn't, because the write may have committed even though the caller never got the response. I simulated that in the last scenario, where retrying without an idempotency key duplicates the write.

So I'd use a stable idempotency key per logical operation, enforced by the service receiving the write, and avoid starting a retry while the original attempt is still in flight.

Collapse
 
shubhradev profile image
Shubhra Pokhariya •

If you want to run it yourself, clone the repo and run npm run verify. There are no dependencies, you just need Node 22 or newer. I only tried Linux and Windows, so if the order of events looks different on your machine, tell me what you got.

Collapse
 
mp3sss31 profile image
mp3sss •

The reproduction table makes the crucial distinction concrete: the aggregate rejects at 50ms, but C still writes to the store at 250ms after the error path has already moved on. Promise.all never had a cancellation model, so the side effects keep running unless the work itself checks an abort signal cooperatively. This matters a lot when batching writes or requests on the desktop side — a partial-failure retry needs idempotent keys so the still-in-flight C doesn't duplicate a mutation, and AbortController only helps when each operation actually listens for the abort instead of trusting the combinator to stop it.

Collapse
 
shubhradev profile image
Shubhra Pokhariya •

Thanks, that's a good summary. One addition on retries: idempotency keys and abort solve different problems.

In my bounded-wait reproduction, task D ignores the signal and still writes at about 250 ms, after the error has already been reported. Even a task that honours the abort may have committed on the other side before it stopped. So a retry has to be safe either way, and the service receiving the write needs to enforce idempotency on its side; otherwise, retries can still produce duplicate effects.

Using "Promise.allSettled()" to collect outcomes and retry only failed operations can also work, but only when a failure is known to mean "not applied". A timeout doesn't establish that.

I only tested Node.js scripts, so I can't say how your desktop runtime behaves. Which HTTP client or SDK are you batching with, and does it actually honour the signal?

Collapse
 
cj_e49fbe4085948741f2f23d profile image
CJ •

This is an outstanding deep dive into a JavaScript behavior that is easy to misunderstand and, more importantly, can create some surprisingly serious production issues.

The distinction you make between the aggregate promise, the individual input promises, and the underlying operations is particularly valuable. I think this is the conceptual gap that causes most developers to get into trouble with Promise.all(). We tend to think of a group of promises as a single unit of work, but JavaScript doesn't treat it that way. The aggregate promise reports the outcome of the group; it doesn't own or control the operations themselves.

Your example of the Save All button is especially relevant because it demonstrates how a seemingly harmless misunderstanding can become a data-integrity problem. A request can fail, the application can report that the entire operation failed, and meanwhile other requests can successfully modify the database. When the user retries, those successful operations might execute again. This is much more serious than simply wasting a few milliseconds of processing time.

I also appreciate that you go beyond explaining the behavior and investigate what actually happens with later rejections. The distinction between an input promise being handled and the aggregate promise itself being handled is an important detail. It's exactly the kind of nuance that makes this article useful as a technical reference rather than another superficial explanation of JavaScript promise combinators.

The section on AbortController is probably my favorite. There is a tendency to treat calling abort() as equivalent to terminating an operation, but cancellation in JavaScript is cooperative. An operation must explicitly support and respond to the signal. Your example of task D continuing to execute and performing its side effect despite the abort request makes that limitation immediately understandable.

I'd also highlight the three separate concepts you identify:

  • Stopping the caller from waiting.
  • Requesting cancellation.
  • Actually terminating the underlying operation.

These are fundamentally different events, and confusing them can lead to some very difficult production bugs.

Another particularly important contribution is the discussion about idempotency keys. I would argue that this is one of the most valuable practical lessons in the entire article. Cancellation is useful for reducing unnecessary work, but it cannot guarantee that an operation hasn't already been committed by a remote service. A network timeout, for example, doesn't necessarily mean a database write or payment operation failed. The server might have completed the operation while the response was lost.

This is why idempotency needs to be designed into side-effecting APIs rather than treated as an afterthought in retry logic.

One additional scenario worth considering is concurrent cancellation and completion. An operation might finish at almost exactly the same moment another task triggers an abort. Depending on the API and where the cancellation is checked, the operation may already have committed its side effect even though the caller observes an abort-related rejection. This reinforces why cancellation should never be treated as a substitute for transaction boundaries, compensation logic, or idempotent operations.

I also appreciate the effort to distinguish specification guarantees from Node.js-specific observations. Including actual reproduction scripts, multiple runtime versions, Windows testing, and clearly documented limitations makes the analysis considerably more trustworthy. Too many JavaScript articles present one runtime's behavior as though it were automatically guaranteed by the language specification.

My biggest takeaway is this: concurrency, cancellation, and consistency are three different engineering problems. Promise.all() helps coordinate asynchronous results. AbortController provides a mechanism for requesting cancellation. Idempotency and transaction design help protect the integrity of operations when failures and retries occur. None of these mechanisms replaces the others.

Here's a question I'd love to explore with other backend developers:

When coordinating multiple independent API calls that can modify persistent data, do you generally prefer Promise.allSettled() with selective retries and idempotency keys, or do you build a dedicated orchestration layer with cancellation, compensation, and explicit operation states?

I think the answer becomes particularly interesting when those operations span multiple services or databases.

Excellent article. This is the kind of detailed, experimentally grounded JavaScript content that helps developers avoid mistakes that might otherwise only become visible under real production traffic.

Collapse
 
shubhradev profile image
Shubhra Pokhariya •

Thanks for reading the article and for the kind words.

On the concurrent abort and completion case, I only touch it in the retry section, where I mention that a cancelled operation may still have committed. I didn't test it as its own scenario, so I can't show a timeline for it. It comes from the same limitation: abort() is a request, and the operation decides how it responds.

On your question, the article covers Promise.allSettled() and retrying only the failures. That approach is safe only when you can establish that a failed operation wasn't applied. A timeout doesn't tell you that. For side-effecting writes that may be retried, I'd use an idempotency key that stays the same across attempts for the same logical operation, whichever approach you pick.

I didn't test an orchestration layer, so the rest is my opinion, not something the repro shows. Within one service and one database, I'd start with Promise.allSettled() plus idempotency keys. Once a workflow spans services and has to recover from partial completion, I'd want the operation states stored somewhere durable, since the settled results live in memory and are lost if the process dies midway.

Do you derive your keys from the logical operation, or generate a new one per attempt?

Collapse
 
oparaugo_michael_f02c4c0d profile image
Oparaugo Michael •

great job.

Collapse
 
shubhradev profile image
Shubhra Pokhariya •

Thanks! Glad you found it useful.