Full-stack developer focused on crafting clean, fast, and scalable web products. I love working with Next.js, React, and Tailwind to build modern, user-friendly experiences.
Location
India
Education
Background in English Literature, self-taught coder through courses, books, and hands-on practice.
Pronouns
Lover of clean UI, simple design, and coding workflows that save time for everyone.
Work
Building clean, scalable web solutions that speed up development and simplify design.
Hosted cron job service. Schedule HTTP requests on a cron, after a delay or right away. Every run logged, failed jobs retried, alerts by email or Slack.
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.
Full-stack developer focused on crafting clean, fast, and scalable web products. I love working with Next.js, React, and Tailwind to build modern, user-friendly experiences.
Location
India
Education
Background in English Literature, self-taught coder through courses, books, and hands-on practice.
Pronouns
Lover of clean UI, simple design, and coding workflows that save time for everyone.
Work
Building clean, scalable web solutions that speed up development and simplify design.
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.
Full-stack developer focused on crafting clean, fast, and scalable web products. I love working with Next.js, React, and Tailwind to build modern, user-friendly experiences.
Location
India
Education
Background in English Literature, self-taught coder through courses, books, and hands-on practice.
Pronouns
Lover of clean UI, simple design, and coding workflows that save time for everyone.
Work
Building clean, scalable web solutions that speed up development and simplify design.
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.
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.
Full-stack developer focused on crafting clean, fast, and scalable web products. I love working with Next.js, React, and Tailwind to build modern, user-friendly experiences.
Location
India
Education
Background in English Literature, self-taught coder through courses, books, and hands-on practice.
Pronouns
Lover of clean UI, simple design, and coding workflows that save time for everyone.
Work
Building clean, scalable web solutions that speed up development and simplify design.
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?
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.
Full-stack developer focused on crafting clean, fast, and scalable web products. I love working with Next.js, React, and Tailwind to build modern, user-friendly experiences.
Location
India
Education
Background in English Literature, self-taught coder through courses, books, and hands-on practice.
Pronouns
Lover of clean UI, simple design, and coding workflows that save time for everyone.
Work
Building clean, scalable web solutions that speed up development and simplify design.
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?
Full-stack developer focused on crafting clean, fast, and scalable web products. I love working with Next.js, React, and Tailwind to build modern, user-friendly experiences.
Location
India
Education
Background in English Literature, self-taught coder through courses, books, and hands-on practice.
Pronouns
Lover of clean UI, simple design, and coding workflows that save time for everyone.
Work
Building clean, scalable web solutions that speed up development and simplify design.
Very detailed explanation of promises and their related functions. Nice work! 👍
Thank you so much! Glad you found the explanation useful. 😊
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.allSettledresults, instead of resending the whole group.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.
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.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.
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?
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
AbortControlleris probably my favorite. There is a tendency to treat callingabort()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:
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.AbortControllerprovides 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.
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?
great job.
Thanks! Glad you found it useful.