-
Notifications
You must be signed in to change notification settings - Fork 45
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
Consider always rejecting postTask() if abort signal received before callback is complete #110
Comments
Super interesting, and excellent summary. As a JS developer I expect that an aborted signal is just a suggestion to abort in the future, provider that: work can actually be saved by doing so. I don't necessarily expect a fully completed task/resolved promise, to get switched from resolved to rejected because of a very late abort. (Aside) ...Using a different example that I had personally experienced before: a framework-level component re-rendering update, which is async and supports abort()... if I try to abort() it due to yet another re-render being posted, I want it to throw any in progress work away, but I might still want to accept the update if its already fully completed and scheduled. However, given the inconsistencies and complexity you point out in the examples, I think your suggestion #2 seems like a better alternative to me. I think. Its complicated :P |
This is a great write-up, thanks. One thing to note is that when browsers invoke a callback (e.g. the Maybe the following is reasonable summary? The promise returned by
But having a simple invariant that "abort --> the promise is rejected unless the async task (function) has finished" is compelling. And to Michal's point, this could save work if it's in a promise chain. Furthermore, introducing So I think I support this, but I'll have to think about/analyze the compat risk. |
There are a few different abort behaviors possible when an abort occurs after a
schedulder.postTask()
callback starts but before it returns, depending on how the callback's execution is divided over tasks and microtasks. This previously came up in #60.I'm wondering if the abort behavior should instead depend only on the fact that the abort signal was received between the start and end of the
callback
, regardless of the content ofcallback
. At least as a JS developer, this would better match my intuition for handing off a promise chain to be slotted into an API's promise chain and then run.Running in Chrome or Firefox (with
dom.enable_web_task_scheduling
enabled):the
postTask
promise will be rejected. Switching the statement order in the callback (which matches the WPT subject of #60), though:and the promise will end up fulfilled.
(these abort the tasks from within the callbacks, but that's just to keep the sequencing simple; the abort could come from a concurrent task)
From #60, there is an argument that this is reasonable behavior from joining the callback's promise chain. Adapting the AbortSignal example from the DOM spec, it would be something like this in JS:
When run with the
// Fulfilled
example above:callback()
is run synchronously up toawait new Promise(...)
and a Promise is returnedresolve()
withincurrentPostTaskish
is called with the pending promise fromcallback()
reject(signal.reason)
from the abort event has no effectcurrentPostTaskish
returns a Promise depending only onresult
, so will end up fulfilledFor the
// AbortError
example, however, the signal has a chance to runreject(signal.reason)
beforeresolve(result)
runs, so the overall Promise is rejected.Two issues I found with this behavior:
First, it appears that not all async code acts identically. Swapping the Promise-setTimeout for an
await
ed constant (so it's only triggering a microtask), suddenly thescheduler.postTask()
call rejects again:Notably this is not what you get from the
currentPostTaskish
stand-in above or thescheduler-polyfill
(they both fulfill, not reject), and not what you'd expect from async/await/Promise chain code in general based on the above reasoning about postTask promise handling (an await is an await forcallback
to return as a Promise, even if only a microtask). I'm guessing it's something about the abort signal propogation being a real task at the browser level vs the event handling incurrentPostTaskish
, but I don't really know. Behavior is identical in Chrome and Firefox (withdom.enable_web_task_scheduling
enabled).Second, it can cause the promise chain to fork when there's an abort signal and an exception is thrown in the callback:
The
scheduler.postTask()
rejects with anAbortError
, but there's also an unhandled rejection for the Errorthrown in callback
.This also shows up in cancelled
scheduler.postTask()
callbacks that contain ascheduler.yield()
that inherit the AbortSignal (Chrome only for now).The
scheduler.postTask()
again rejects with anAbortError
, but now the unhandled rejection is from the sameAbortError
instance.Finally, since code could be handed an arbitrary function to run in a
scheduler.postTask()
(so it won't be known ahead of time which behavior above will be triggered by a mid-task abort signal), it seems better semantically that the signal is interpreted as either triggering rejection up untilcallback
starts execution or up untilcallback
has finished:callback
execution, socallback
will continue to execute to its end. Therefore thescheduler.postTask()
promise should be fulfilled to match that the callback finished executing without errorsignal.throwIfAborted()
if I need more break points. However, the task was aborted and regardless of what ended up executed, the returned promise's resolution should reflect that fact so subsequent code can react accordinglyI personally find the second approach to be the more compelling, especially because it means subsequent code can also be cancelled without plumbing the AbortSignal everywhere (which is part of the point of the async chain).
I didn't find much guidance on abort semantics out there, but this also seems to be at least "encouraged" by the spec:
To accomplish this,
postTask
would wait until the return value ofcallback()
is resolved before resolving or rejecting the returned Promise. The JS equivalent would be something likeThe text was updated successfully, but these errors were encountered: