Repository navigation
Thrown AbortErrors are not DOMExceptions #40692
Description
Activity
Hey, this is by design, you can see the context and meeting in #36084. Node.js does throw
DOMExceptions for DOM APIs. For its own APIs Node.js prefers a simpler AbortError with the same.nameproperty (which is the preferred way to check forAbortErrors according to the DOM spec). The reason for this is that Node.js vendors some of its modules (like readable-stream) and having to shipDOMExceptionwith them would be hard.Reacted by Tony Gorez and 流浪大法师@benjamingr That design does not appear to have been revised when node added support for native
DOMException. This is causing extra confusion, and the root of this issue.Essentially, this decision puts the onus on API consumers to handle the strange complexity, and on API documenters to clearly convey this, just so node can potentially simpler vendor readable-stream (which has not actually happened due to other complexities).
Given that there is no obvious central place to document this complexity, any API that uses a
signalmust clearly communicate this, which is currently failing.I hope you will take a chance to reconsider this. You are prioritizing the efforts of a few core developers against the documentation team, and the entire developer base.
Reacted by Domenic Denicola, 流浪大法师, Ivan Kleshnin and vladimir1001@benjamingr That design does not appear to have been revised when node added support for native DOMException. This is causing extra confusion, and the root of this issue.
Node has added DOMException before AbortSignal - the decision was to move away from DOMExceptions for non DOM APIs using AbortSignal and stick to .name (the reasoning is in the thread I linked to and in the meeting minutes).
(Node does vendor readable-stream as the readable-stream package on NPM btw - but it hasn't been updated)
Given that there is no obvious central place to document this complexity, any API that uses a signal must clearly communicate this, which is currently failing.
Would it help if the documentations clarified the
AbortErrorinterface we use like we do forAbortSignaland friends?I hope you will take a chance to reconsider this. You are prioritizing the efforts of a few core developers against the documentation team, and the entire developer base.
I don't think that's an entirely fair or an accurate representation of how things transpired:
- AbortController/AbortSignal was added to the platform to enable cancellation scenarios.
- We iterated on them and added them to more APIS while soliciting responses from the community, library authors, WHATWG folks and other subject-matter-experts.
- When soliciting feedback from library vendors and core - the concern about DOMException being complicated and different was raised. There were no objections to changing it - you are in fact the first person to bring up this complaint in the ±year we've shipped this.
- The process was open and in the public, anyone from the community was welcome to participate and many were invited specifically so we collect user feedback. We've had a meeting about this in the summit (online, also open to everyone) and several other discussions.
- This decision was made considering the people who built AbortController/AbortSignal/AbortError in the DOM in the first place, people who worked on other AbortSignal APIs, the community, library authors and more.
- Things quieted down pretty quickly and we're getting positive feedback.
Also I want to point out that the decision isn't final, we can revisit anything here. We did not receive any pushback about this change from the documentation team nor developer base. We are happily willing to listen to feedback.
All of this is relatively new and improvement opportunities (in docs, APIs, debugging experience and more) are welcome. Contributions are welcome and Node.js in general and this area in particular is eager to receive them.
Thanks for the thorough response, and good to know that there is still room for iterating the approach.
DOMExceptionhas only just been made public with node v17, so has only been there for a few weeks for API consumers. It was merged August 6'th in e4b1fb5, and I don't see any considerations on how this impactsAbortErrorin any of the relevant PRs. Issue #39098 explicitly mentions the ability to doinstanceofchecks. #39176 can hardly have been a consideration almost a year ago in #36084.Also, most feedback you will have gotten so far is likely not from the wider dev community, as many still support node v12, and are only just starting to look into using the features v14+ provides due to v12 EOL coming up.
FYI, I'm very exited about this new feature, which is why I want to see it done right. Here I think unifying on
new DOMException(message, 'AbortError')for node native and modules targeting v17+ will make it simpler to use. Both as providers (those throwing) and consumers (those passingsignal/ callingabort()) that are likely to have to ignore the subsequent rejections. Besides making it simpler to use the feature, it also enables compatibility with browser runtimes.This still leaves module that target v14/v16 and v17+ "in the dust", having to deal with extra complexity to handle two different error implementations. However, they are no worse of than the current situation, and will eventually be able to drop the legacy codepaths.
Given that there is no obvious central place to document this complexity, any API that uses a signal must clearly communicate this, which is currently failing.
Would it help if the documentations clarified the
AbortErrorinterface we use like we do forAbortSignaland friends?Yes – this would be simpler to document if the internal
AbortErrorwas public (as discussed in #38361).Thanks for the thorough response, and good to know that there is still room for iterating the approach.
Absolutely.
DOMException has only just been made public with node v17, so has only been there for a few weeks for API consumers. It was merged August 6'th in e4b1fb5, and I don't see any considerations on how this impacts AbortError in any of the relevant PRs. Issue #39098 explicitly mentions the ability to do instanceof checks.
It's true that DOMException has been made public in Node.js 17 but it has been in core for a few years now (since things like URL raise it). Note that a public DOMException is even worse for stuff like
readable-streamthat exists for compatibility across versions since people may expectinstanceofchecks to work (and they won't) - neither will instanceof checks work across vm contexts and other "realms".In general it makes perfect sense to expose DOMException as well as AbortError but people should never rely on
instanceofchecks for errors. That's why there were proposals for stuff likeError.isError.If you read the WHATWG fetch specification carefully (I am happy to dig up the discussions where we initially met to discuss AbortController) - you will see it makes no requirements on raising
AbortError. In fact when I added an API myself (signal in addEventListener) there were no errors involved.DOM APIs are encouraged but not required to reject with an AbortError DOMException - non DOM APIs have no such requirements. If you look at the spec example it encourages checking
.namerather thaninstanceof(for the aforementioned reasons) that is quite intentional.#39176 can hardly have been a consideration almost a year ago in #36084.
I actually recall seeing (but not having time to review) that and trusting reviewers since I think it's overall a good idea.
Also, most feedback you will have gotten so far is likely not from the wider dev community, as many still support node v12, and are only just starting to look into using the features v14+ provides due to v12 EOL coming up.
This was less "users approaching us" and more "us approaching users and asking what they think" and "us talking to people who have experimented with userland
AbortSignals (like SDK authors) and asking them what feedback their users are giving. Gathering user and library maintainer feedback is always a big challenge in Node.js and always something we could use more of.FYI, I'm very exited about this new feature, which is why I want to see it done right. Here I think unifying on new DOMException(message, 'AbortError') for node native and modules targeting v17+ will make it simpler to use. Both as providers (those throwing) and consumers (those passing signal / calling abort()) that are likely to have to ignore the subsequent rejections. Besides making it simpler to use the feature, it also enables compatibility with browser runtimes.
Note that we switched from DOMException - I think this is where it was added after the meeting and consensus.
Unless the concerns raised by @mcollina regarding this being hard for readable stream and other parts were addressed in his perspective - I don't see us switching back to DOMExceptions (which wouldn't break a lot of code probably since people are supposed to check
.namebut it might).Yes – this would be simpler to document if the internal AbortError was public (as discussed in #38361).
Is that something you'd be interested in contributing? I know we've talked about this in the past but I am honestly not sure what it's blocked on and I am happy for us to start with a simpler flagged errors module and add more codes/errors.
How can I help you with this? (I can review, ping the TSC to discuss the module inclusion, try to get more people to look at this etc).
I don't have much time right now for joining a conversation with this depth. From a quick skim @benjamingr summarizes this correctly. My position has not changed on this matter.
Reacted by Benjamin GruenbaumWe agree that it makes sense for user code to distinguish
AbortErrorfrom regular errors, eg. to avoid logging it, right? Thus it makes sense that the error included in the rejection (when triggered) is clearly identifiable. As you say, this is currently done through checking the.nameproperty, but it relies on APIs to reject using properly crafted errors.While the node APIs can be aligned on what is thrown, it is anyones guess what third-party modules will throw. And since those modules might forward the
signalto submodules, the same API call could be able to reject using two or more completely different abort errors, depending on when it is aborted. Ie. you can't even reliably test against how abort errors are emitted.For a different take on this, node could expose helpers like this to enable tests for
.abortedand throwing using a standardizedAbortError, making it less likely that third party modules get it wrong. And probably also something for the rare cases that the 'abort' event is used. This can be done without exporting theAbortErrorclass.Btw, this issue is really about standardizing the rejected error, be it through always using
DOMExceptionor some other means. I'm naturally inclined towardsDOMExceptionsince it better aligns with browsers, but if it is a clear no-go, then there are still other ways to improve the situation.Reacted by Benjamin GruenbaumI think we are all very much in favor of standardizing third-party thrown exceptions and providing guidance to people authoring async APIs with cancellation. I think that's a good idea and I don't think that was contested.
I think that is mostly blocked on someone doing the work. If exposing
errorsis a problem (I think it can be done and think the block is just technical) I am fine with exposing it elsewhere.- addederrorsIssues and PRs related to JavaScript errors originating in Node.js core.Issues and PRs related to JavaScript errors originating in Node.js core.
on Nov 6, 2021 I saw claims like this a few times:
If you look at the spec example it encourages checking .name rather than instanceof (for the aforementioned reasons) that is quite intentional.
I can state that is not intentional, and using
.nameis not the recommended way. That one example in the DOM spec is just an example. All coding styles are valid and OK on the web;instanceof, for example, is fine (if you're working within a single realm). As is checkingerr.code === DOMException.ABORT_ERR.I do think it would be beneficial to the ecosystem if Node didn't create its own separate AbortError type, and instead used and encouraged the standard DOMException. I think as @kanongil points out the fact that there are two is going to cause a lot of ecosystem confusion, as e.g. code that wants to be web/Deno compatible uses DOMException, and code which doesn't care about such compatibility uses Node's one-off version, and so on.
Reacted by Derek Lewis and Oskar Larsson HögfeldtWhy would it be a good thing for environments without a DOM to have or use a DOM exception? If it was meant to be a universal exception type then it seems it was unwisely named.
Reacted by Derek Lewis and Ivan KleshninYes, it's pretty easy to complain about names of things in the JavaScript ecosystem; as I'm sure you're aware we haven't named everything optimally in its 20+ year history.
Reacted by Derek Lewis, Jordan Harband, Matteo Collina, Benjamin Gruenbaum, Debadree Chatterjee, Ethan Resnick and Paul Draper@domenic to be clear the thing blocking
DOMExceptions for AbortErrors in Node.js isn't the fact people don't likeDOMExceptions (we already shipDOMExceptions anyway).It's the fact
DOMExceptions become a dependency of libraries (likereadable-stream) that are explicitly designed to run in non-modern Node.js environments and portingDOMExceptionthere would be hard.The current solution is a compromise that enabled us to adopt
AbortSignals in all APIs while staying spec-compliant. Obviously we would be happier shipping less kinds of errors for the same thing :)Why would it be a good thing for environments without a DOM to have or use a DOM exception?
It's a communications thing but IMO the
DOMness of the exception is "this exception comes from a DOM API" rather than requiring a DOM tree.Oh, and regarding:
I saw claims like this a few times: I can state that is not intentional, and using .name is not the recommended way. That one example in the DOM spec is just an example.
@domenic I think we got that feedback/best-practice from @jakearchibald both in the AbortError meeting regarding it and back when cancellable
fetchwas discussed since it inter-ops between realms and is guaranteed.If there is other preferred guidance we are happy to look at alternatives (though it would have been great a year ago).
(Also note I am happy to ping you more on these issues but I know you're busy and remember you don't like direct pings so I've been limiting the amount of pings to cases spec-compliance was on the line)
(And of course the thing that blocked using DOMExceptions wasn't their name anyway it was the difficulty vendoring)
@benjamingr "came from a DOM API" means we'd be expecting users to know the difference between specifications, something web browser representatives have repeatedly insisted they do not and should not understand.
I'm not saying the name of DOMException is the issue; as much as it is that AbortError inherits from it. Is it really too late for the web to fix that?
43 remaining items
It's unfortunate however Node.js can't take all its APIs and change their exception surface from "a standard node error with a .code property" to "whatever the user can throw to you"
Why not? The only way to observe this is if you pass a signal that throws a different error, in which case you are presumably expecting that signal's error to propogate. Again this is a major part of the the reason that
.abortaccepts a reason at all.and not to the better.
I disagree, being able to signal different kinds of exceptions through is useful. Like
AbortSignalitself already offersAbortSignal.timeoutwhich throws aTimeoutErrorinstead so that one can discrimate such cases.It basically means every error handling code that may work with signals must expect anything to be thrown from whoever owns the controller.
Code that sees exceptions it doesn't recognize should just rethrow them and stop continuing work, this is basic code design that has been understood for decades at this point.
Reacted by Domenic Denicola and Randall LeedsReacted by Domenic DenicolaWhy not? The only way to observe this is if you pass a signal that throws a different error, in which case you are presumably expecting that signal's error to propogate.
Yes but you do not own the controller just the signal. So someone from the outside can change your API surface.
If you have error handling code that does something like:
function myCode({ signal }) { try { await someFunction({ signal }); } catch (e) { const aborted = someCheck(e); // e.g. e.name === 'AbortError' || e.code === 'ECONNABORTED"; if (!aborted) throw e; else handleCancellation(e); const retryable = someOtherCheck(e); // e.g. e.code === 'EAGAIN' await retryLogic(e); throw e; } }
In this case - every error may become unrecoverable and unexpected from the signal. The controller isn't aware of all its consumers. Let's say my code does something like:
const ac = new AbortController(); startServer(someData, ac); // gets AC to abort on request await myCode({ signal: ac.signal });
If
startServer(potentially one library) changes the signal to throw a different error the completely unaware myCode (potentially another library) has to change its logic to deal with it. It basically increases the expected error surface of the method to anything. A lot of people writing Node.js rely on being able to know what errors are thrown from APIs so I am very unsure we can say "our APIs may reject/error with anything now if you use our cancellation primitive" and get away with it.and not to the better.
I disagree, being able to signal different kinds of exceptions through is useful.No argument regarding it being useful the disagreement is only about the core APIs rejection/error API surface. Something being useful doesn't mean it doesn't come with painful tradeoffs and I believe Node.js can't make this one for the reason above.
Code that sees exceptions it doesn't recognize should just rethrow them and stop continuing work, this is basic code design that has been understood for decades at this point.
You are telling me that people don't write great code - as the platform I'm not sure what to say other than "Hyrum's law". There are many things I wish we could discourage more in terms of the code people write (rejecting/throwing with strings anyone?).
Node servers crash when an exception happens, crashes can cause a DoS, cascading failures, financial damage etc. If some library I don't control using core APIs decides to abort a controller with a new type of cancellation/error - I get woken up at night (bad) or go down (even worse). I got woken up for forgetting to filter cancellation errors correctly from instrumentation back in 2014 times ^^
(Ironically enough (all parties were for it iirc), if we had third party cancellation this wouldn't be an issue ^^)
(Also @domenic as you 🚀 ed @Jamesernator comment, I want to say that while I disagree with James's point I do see it. I'm happy to change my mind, debate this and hear more arguments. Your inputs here are always valuable and respected. I hope the ±10 years we've interacted about these sort of issues are enough to convince you I am sincere in that and am always happy to hear you out)
And let me ask for some data:
@mcollina @ronag from the
readable-streampoint of view is DOMException in Node.js old enough that in terms of support we can ship readable-stream relying on it being available?From the API side, I'm wondering if there is some way to find out how common "I expect this list of errors" code is as well as find out how many people treat cancellation differently than exceptions in their error handling code. I'm happy to hear ideas on how to measure this and change my mind if I'm assuming Hyrum's law and being pessimistic. One thing we may be able to try is a flag to change the behavior and then to ask people to report issues with it. I'll also see if I can ask the Node.js using teams/groups in Microsoft to check.
So someone from the outside can change your API surface.
Yes, but again again this is the whole point of being able to signal any error.
If you have error handling code that does something like:
function myCode({ signal }) { try { await someFunction({ signal }); } catch (e) { const aborted = someCheck(e); // e.g. e.name === 'AbortError' || e.code === 'ECONNABORTED"; if (!aborted) throw e; else handleCancellation(e); const retryable = someOtherCheck(e); // e.g. e.code === 'EAGAIN' await retryLogic(e); throw e; } }
This is exactly the sort've fragile pattern I was talking about, you shouldn't be handling cancellation specifically, EVERY exception that can't be handled explicitly should result in the work being cancelled not simply those with name
AbortError.A better pattern is to precisely reverse this sort've pattern:
function myCode({ signal }) { try { await someFunction({ signal }); // Note we don't handle cancellation specifically at all } catch (e) { // If it's not a retry, then we don't know to handle it, so we just cleanup and stop work if (e.code !== "EAGAIN") { await cancelWorkAndCleanup(); throw e; } // Whatever to retry await retry(...); } }
If startServer (potentially one library) changes the signal to throw a different error the completely unaware myCode (potentially another library) has to change its logic to deal with it.
If you're giving
startServerfull control of theAbortControllerthen yes it can throw anything, this is just the nature of allowing.abortto take anything.If you don't want this, pass a different
AbortControllerand follow it instead:// Yes, I don't like how verbose this either, the AbortController/AbortSignal API does leave // a lot to be desired in the sugar department const ac = new AbortController(); const serverController = new AbortController(); serverController.addEventListener("abort", () => ac.abort(new WhateverIWantToAbortWith())); startServer(someData, serverController); // gets AC to abort on request await myCode({ signal: ac.signal });
If some library I don't control using core APIs decides to abort a controller with a new type of cancellation/error - I get woken up at night (bad) or go down (even worse). I got woken up for forgetting to filter cancellation errors correctly from instrumentation back in 2014 times ^^
There are general problems with JS async error handling that leaves a lot to be desired, but generally speaking cancellation errors shouldn't be propagating to a point where instrumentation could even observe them.
I don't know what cancellation primitives were around in 2014, but for
AbortControllerif you create an abort controller and pass the signal around, it should be expected that whatever you pass it to might raise an exception.Like in the given example from before, given the code seems to be top level one should be prepared to handle the exception:
const ac = new AbortController(); startServer(someData, ac); // gets AC to abort on request // We handle cancellation at this level because we are the ones who are prepared to handle cancellation try { await myCode({ signal: ac.signal }); } catch (e) { if (e === ac.signal.reason) { // The code has been cancelled, so we don't need to throw an error } }
Something else I do think is worth pointing out in all of this, is that for the most part users shouldn't be needing to handle cancellation at all in intermediate functions because generally speaking user code will be delegating to Node code that actually acts as sinks of
AbortSignals.Like to demonstrate what I mean, if we write something that can be cancelled:
async function doSomething({ signal }) { // ... }
then internally in most cases
doSomethingwill be passing thesignalto functions created by Node which act as sinks ofAbortSignal, e.g. things likefetchorfs, etc, etcasync function doSomething({ signal }) { // ... await fetch(..., { signal }); // ... await fsp.readFile(..., { signal }); // ... await timerPromises.setTimeout(..., { signal }); }
here users don't need to handle cancellation at all, they just let the various Node APIs throw the error and it'll propagate up naturally.
Also, usually speaking when users are implementing abortable APIs directly, the web-like behaviour is basically encouraged thanks to the exposure of
signal.reasonand.throwIfAborted()(also mdn docs recommend it, so most users will fall into this path as well):// Supposing userland implemented timerPromises.setTimeout themselves function setTimeout(delay, { signal }={}) { return new Promise((resolve, reject) => { const timeout = setTimeout(() => resolve(), delay); signal?.addEventListener("abort", () => { clearTimeout(timeout); // Note that the easiest available option here is to use signal.reason reject(signal.reason); }); }); }
Reacted by Randall Leeds@Jamesernator you are giving me examples of how one can author code to avoid pitfalls this is potentially creating, my argument hasn't been that it isn't possible to write code that works after this change it's that a bunch of people won't and likely already don't. That was the whole bit about Hyrum's law and errors being part of the API surface (enough that error code changes in Node are semver major and we "broke the ecosystem" a few times in the past before (and after) that).
(Also it's depressing to me that we've given people an API where even experienced developers debating the semantics of these very APIs forget to remove the event listener on the signal, myself included sometimes)
after this change it's that a bunch of people won't and likely already don't.
Well they certainly won't if Node decides to actively support behaviour that is divergent from what web browsers implement and the spec recommends.
Also
.reasonwasn't initially part of web implementations either, yet they managed to ship this change perfectly fine. I don't see why Node should be in anyway special here.That was the whole bit about Hyrum's law and errors being part of the API surface (enough that error code changes in Node are semver major and we "broke the ecosystem" a few times in the past before (and after) that).
I'm not suggesting this shouldn't be a semver major change, though I am wary of the fact that the longer this behaviour is around the more entrenched it becomes.
And it's not like people in Node can even handle such errors in a consistent way anyway, as I pointed out with my earlier example. Ultimately if people rely on Node's error behaviour, and a library changes to use
fetchor some other web API, then they will be hit with the web behaviour anyway.forget to remove the event listener on the signal, myself included sometimes)
Yes, I usually use a helper that cleans up the listener. There are suggestions for this problem in the DOM spec, though they all have the dreaded
needs implemeter interesttag.(Also it's depressing to me that we've given people an API where even experienced developers debating the semantics of these very APIs
I don't want this to come across as an attack on anyone, but I do really think whatwg dropped the ball when designing this API. It's beyond me why the fairly obvious problems were not considered at the time, and still fail to have implementer interest to fix.
It also has always strongly bothered me that from the get go of
AbortSignal, the spec had a special hook to specifically avoid the event behaviour which doesn't suffer any of the same problems.Also .reason wasn't initially part of web implementations either, yet they managed to ship this change perfectly fine. I don't see why Node should be in anyway special here.
Well, the web has a lot fewer APIs than Node that work with AbortSignal and (arguably) a lot more use cases where you'd need to abort ongoing actions.
As I mentioned I am happy to change my mind if my intuition of "this breaks things" is wrong and I'm not sure how to get the data
It also has always strongly bothered me that from the get go of AbortSignal, the spec had a special hook to specifically avoid the event behaviour which doesn't suffer any of the same problems.
There are actually two now whatwg/dom#1195 the recommendation is (?) to do
AbortSignal.any([signal]).addEventListener. Honestly this sucks and I can complain but I don't have the time to fix this and the reason it hasn't been fixed/addressed isn't bad faith but funding - I'm confident that if someone spent a bunch of time improving this it will be improved.As
Symbol.dispose/Symbol.asyncDisposeare stage 3 we may move to those APIs for resource management (streams, file handles, servers etc) and deprecate/remove AbortSignal support in those APIs (but not ones that signal the cancellation of an action, we'll keep using AbortSignal for those). For AbortSignal we may want to provide a helper that uses our internal (unfortunate) machinery we have for "non stoppable and weakly held event listeners" that user-land can't make atm spec wise otherwise (both weak and "unstoppable" events not being part of the spec, again somewhat for funding).Currently readable-stream supports down to v12. However, the next release would likely cut down to v18, so this can land in a semver-major change.
Reacted by Benjamin Gruenbaum, Jacob Hummer and Randall LeedsComing back to check in on this issue. In my own code, most of my use of Node.js APIs that take signals was using Node.js streams. I've switched these all to use the Web Streams API and now I can reliably compare exceptions to the abort reason.
I do think it's pretty confusing that Node.js has a mix of behaviors here. If it isn't already too entrenched to consider changing this, maybe we want to try to do it for v24 now that the v23.x branch is cut.
Reacted by n0isygithub-actions commented
on Jun 26, 2026 on Jun 26, 2026 – with GitHub ActionsContributorMore actionsThis issue has been marked as stale due to 210 days of inactivity.
It will be automatically closed in 30 days if no further activity occurs. If this is still relevant, please leave a comment or update it to keep it open.- addedstaleIssues and PRs marked stale due to inactivity and scheduled for automatic closure.Issues and PRs marked stale due to inactivity and scheduled for automatic closure.
on Jun 26, 2026 github-actions commented
on Jul 27, 2026 on Jul 27, 2026 – with GitHub ActionsContributorMore actionsThis issue has been automatically closed after 30 days of inactivity following its stale status (no activity for a total of 120 days).
If this is still relevant, feel free to reopen it or leave a comment with additional details so we can continue the discussion.
Version
v17.0.1
Platform
any
Subsystem
No response
What steps will reproduce the bug?
How often does it reproduce? Is there a required condition?
100%, including for all other native APIs that supports signals.
What is the expected behavior?
From the docs:
There is no mention that this is a non-standard
new DOMException(message, 'AbortError')error. As such, I expect 'REF' and 'ERR' logged objects to be the same (except the stack).What do you see instead?
Additional information
This is only an issue in node 17+, since previous versions did not have a
DOMExceptionglobal.