-
Notifications
You must be signed in to change notification settings - Fork 297
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add a way to identify Abort errors originating from controllers #1033
Comments
Won't it always be safer to check async function a({ signal }) {
await b();
}
async function b() {
const ab = new AbortController();
ab.abort();
throw ab.signal.reason;
}
try {
const ac = new AbortController();
await a({ signal: ac.signal });
}
catch (err) {
console.log(err); // 'AbortError' DOMException
console.log(ac.signal.aborted); // false
} |
If you have a direct reference to the signal you might. You often do not have a reference to the signal which means you can't check Think "I'm instrumentation that needs to decide if an error is really exceptional and I need to wake up someone in the middle of the night or I'm just cancellation which I will happily ignore". |
I don't agree with this claim – especially the "often" part. Do you have any concrete examples where you need to handle abort errors without access to the |
Hmm, it's not actually possible to brand all reason values, as any non- |
@kanongil In this case "instrumentation" is something like APM (that presumably doesn't override We have spent time talking to API consumers about this and have plenty of examples and use cases. Here is a common one (I'll use the web terminology since this is whatwg/dom but there are plenty of others).
This is just an example, it can be worked around (by for example making the signal global, injecting the signal to all components) but that would create pretty ugly code from what I recall. I am happy to loop in library/sdk authors we have talked to about this in the past if that'd help. |
It would be great if we could prohibit non |
I think the premise here is wrong. If the controller wants to send an "AbortError" DOMException, knowing some consumers will discriminate based on that, it can do so. But if it wants to send a TypeError, knowing some consumers are not specifically watching for that, it can. You should generally not write code that assumes all controller-caused errors are "third state" abort errors. The signal is telling you about an error, but that error has the usual semantics based on its type: some are "third state" abort errors, some are fatal failures of a different sort, some are timeouts, etc. Perhaps it would have been better if AbortSignal were named ExceptionSignal, but the historical naming here shouldn't cause people to assume all controller-initiated exceptions are treated the same way (e.g. ignored). |
Why is that capability good/useful?
Are there any non-third-state errors that are aborted with? |
That capability is useful in the same way that exceptions in general are useful. A signal-accepting API is one which gives its caller some amount of control over what exceptions it throws/rejections it returns/etc. Those exceptions can communicate anything: sometimes an abort, sometimes a timeout, sometimes a TypeError, sometimes a HTTP error (in a Node server), etc. The caller could use its controller to control the function and have it give a TypeError in exactly the situations a TypeError would normally be appropriate. (E.g., the caller has done some further async validation work and realized that the inputs are bad.)
Any error can be the rejection reason. That's the point. When you accept a controlled signal, you are just giving the caller the ability to participate in your function's control flow. |
Stated another way, the fact that a function accepts a signal should not change how you handle its errors. If you expect a function to return error types {X, Y, ...other stuff you don't care about}, and you want special handling for X and Y, then just add special handling for those, and rethrow the rest. It doesn't matter which among X/Y/other stuff come from logic provided by the controller, vs. the function body. |
Just to point out another example, accepting an i.e. Like this could throw any error: async function someTask(transformResult=(result) => result) {
const data = await loadDataSomehow();
const transformed = await transformResult(data);
return transformed;
} If you're not in control of the |
@Jamesernator that's a good analogy but you'll find few (if at all) places in the DOM that have this behaviour. Event listeners for example "wrap" error handling internally and call the host error handler rather than "throw it back" to the @domenic I think I was confused about how this works? If a user does const ac = new AbortController();
const fetchResult = fetch('.../', { signal: ac.signal });
ac.abort(new TypeError()); Is |
The goal is to reject with the reason. #1030 tracks this. |
@annevk isn't that a big breaking API change potentially changing how code passing an external signal to I am wondering if I am missing something since it feels like the API surface of AbortSignal Would Node.js still be at liberty to not reject with |
I don't see how it's breaking. Any existing code should still work the same way. If one part of your code adopts something new and another part does not, that might result in breakage, but that seems expected. If Node.js did that it would go against https://dom.spec.whatwg.org/#abortcontroller-api-integration. Note that @jasnell also reviewed this addition. |
I also don't see it as breaking in the general case. Node.js has, for some time, used our own Node.js (and any application) is at liberty to ignore the new Where developers might need to bridge those environments, at least in Node.js, it would be a good idea for Node.js' |
I am not sure Node.js will align (there will be significant resistance internally), remember: I am not the one who pushed Node's own AbortError and those parties will have to be convinced or we will have to end up with a non-spec-compliant signal. Our consensus based governance is that blocking is very easy and the TSC doesn't like calling shots when there is resistence. That said: I believe we can handle Node.js - I was more concerned with everyone on the web using I am honestly kind of terrified by this change and don't have the capacity to "dig into" this since I'm stuck home with a sick kid who won't let me get more than 10+ of computer time :) Once I get more time I'll probably spend it on symbols in signals since that will teach me more about WebIDL and there appears to be consensus. That said, to avoid the breaking change I think would make sense to:
That way we fix the problematic terminology, end up with a clearer API and a smaller one with better API guarantees. Then Node.js can choose which APIs get ExceptionController and which do not. |
Also, while I understand we (node) do not need to fulfill the AbortController integration bits since those only apply to web APIs - I would much rather talk this out (when we can) and figure out what I (or others) are misunderstanding. The fact Node.js "can" implement its own APIs while staying spec compliant with |
That seems like an awful lot of complexity. Thus far there has been no problem adopting this change for the various operations that consume an Do you still think we need to keep this issue open? |
This one? Sure - I am still interested in a way to detect an error originated from a signal (tagging or other) so that Node can provide a way for users to migrate from checking `err.name === 'AbortError'. |
The design doesn't allow for that. At least, it's not clear to me how you'd do that for |
Indeed. Without access to the Also, is it really an issue? If someone does |
It can actually be an issue for library code that does have access to the |
Apparently, when I went around asking library authors and project members identifying AbortErrors was important to them so I am here representing them figuring out if we can do this.
I would have honestly much preferred if it was only possible to abort with Something like: // Detect if an error was signaled as a cancellation reason
// throws TypeError if `err` isn't an Error (or DOMException, but preferably error)
// returns true if the error was tagged as signaling cancellation, false otherwise
AbortController.isAbort(err); I realize this can be done in Node.js land as a static method - but I don't think this is a Node.js specific concern. |
@benjamingr thanks, I think that's a concrete suggestion that could work. Let's leave this open to track interest in that. I think it would help a lot if those library developers could make the case for this feature as at least for me it's somewhat hard to do so given what's been presented thus far. (It might also be worth moving this to a new issue as this contains a lot of other discussion, but I'll leave that decision up to you.) |
@annevk Thanks - I feel like I need to communicate this. I know you to be very reasonable and I understand it is terrible timing to come here with this problem after the feature landed and James gave an LGTM. I could have raised concerns during that period and missed it - I acknowledge that. I am not trying to create frustration and I am personally very invested in aiding AbortSignal/AbortController succeed as APIs in many environments. I think this is represented in the (many) APIs we've integrated it into. I just think I've found an issue with the API change that can cause issues and I feel like I need to make sure we don't create issues for users. I also acknowledge that it's entirely possible this is just a misunderstanding on my part and that in practice people will only abort with I will ping the people who have spoken up about this in the past. |
Some members (e.g. @littledan) are on a break so unfortunately it's going to be hard to get their feedback. Pinging @benlesh @ronag @bterlson @mcollina @jakearchibald in the meantime - if no one response I will ping 4 more (in level of engagement in the past to not create a lot of pings) :) Let me know if you want additional context and I'll write a summary. |
I think the design was too specialized before this discussion, and it seems to be getting more specialized. Personally, I'm unsure of the value of passing a reason to Providing a new |
I'm still strongly against the direction @benjamingr is pushing and believe it's based on a misunderstanding of this feature. But I've made that case already in these comments #1033 (comment) so I won't repeat myself. I just wanted to be clear I am against any additions to address this use case since I think it's a bad use case. |
I generally agree with @domenic on this, although I might be more convinced about the need with some concrete examples of code that can't access the signal but still needs to observe cancellation. Being able to identify cancellation from sources you don't have a signal/controller for seem a lot like arbitrary stack inspection, like the fact that an abort controller/signal is even used should generally be transparent to you unless you're actually in a position to observe it. The one possible exception to this is promise-accepting APIs, however these are pretty rare, more often things will take a function which returns a promise in which case you should have an opportunity to pass in an abort signal/controller.
While I do agree with the sentiment, the fact that cancellation had to be done in userland and efforts at TC39 failed, means that wiring abort signals through all functions is just what is neccessary. I would certainly still like to see some revival of such integration of cancellation, but I'm not sure it's likely to happen. If function decorators happen at some point in future, it may be possible to create such a thing in userland (especially easy if we could get an This is yet-another place where |
Let's be clear: the use case being asked here is not observing cancelation. It's observing who caused an arbitrary exception. In the following example: const controller = new AbortController();
const resultPromise = fetch(url, { signal: controller.signal });
setTimeout(() => {
controller.abort(new TypeError("actually, it turns out the URL was bad in some application-specific sense, and we didn't validate it until now!"));
}, 10);
Code that cares about cancelations (abortions) should check for Fundamentally, code which is inspecting the implementation details of how a given exception appeared, is unsound. I like @Jamesernator's comparison to stack inspection; you shouldn't need to do either such thing in order to handle exceptions. |
@domenic Checking the error is not sufficient if you don't control the export async function retryableGetBlob(url, { signal, retries } = {}) {
try {
const res = await fetch(url, { signal });
if (res.status !== 200 && res.status !== 204) {
throw new BadResponseError(res.status);
}
return await res.blob();
}
catch (err) {
if (signal?.aborted) {
throw signal.reason !== undefined ? signal.reason : new DOMException('User abort', 'AbortError');
}
if (!retries || isFatalError(err)) {
recordUpstreamFailureMetric(err);
throw err;
}
// retry on soft errors
return retryableGetBlob(url, { signal, retries: retries - 1 });
}
} Here you don't want to call This is probably how it should be, and I like the power that Footnotes
|
@kanongil I think @domenic 's point is that the fact a signal originated from a controller is by design opaque to you and cancellation is still going to always be I think this is probably fine but the guidance (always throw |
(I'm reaching out to library authors and core members who strongly objected to this in the past to see if their opinion changed and we can just close this) |
I’m a little concerned that this will break assumptions and make the flow harder to reason about. How about keeping it as AbortError but reason goes as either the message or a property on the aborterror? Or maybe use the existing cause property? |
That seems backwards. If anything, code listening for the abort would throw their own errors with signal.reason as the cause. E.g. signal.on('abort', () => {
throw new Error('...', { cause: signal.reason }); |
@ronag that is likely what we'll do but that is not the guidance and in my experience when there were misunderstandings like this it was 90% just a misunderstanding. Intuitively this indeed does greatly increase the API surface of all of our APIs taking signals. The guidance to @jasnell note that in this case
So while we can keep |
I'm closing this as working as designed. We decided that |
Sorry for the very, very late response here, but actually this likely is fine for RxJS's needs and may help discriminate cancellations that are expected from unexpected cancellations. For RxJS's part, we might internally Now, that said, the only real concern I see is the fact that in many cases |
Hey,
I recently realized that it's now impossible (since
.reason
and.abort(reason)
) to identifyAbortError
s, it would be really useful if there was some way to identify them as originating from cancellation (timeout is cancellation in that it's not an exceptional case that should warrant monitoring probably).It would be useful either to have a property on
DOMException
s that signal cancellation/timeout users can check or a branding checkAbortSignal.isAbortError
or similar.This would enhance the ergonomics of using
AbortSignal
s while allowing the flexibility of passingreason
.The text was updated successfully, but these errors were encountered: