Skip to content

Conversation

@jakearchibald
Copy link
Collaborator

@jakearchibald jakearchibald commented Nov 20, 2025

This PR is a discussion starter. The goals:

  • Make progress on allowing TC39 able to adopt/use AbortController and AbortSignal without requiring Event and EventTarget.
  • Provide a system where abort 'listeners' can be GC'd as soon as abort has happened.

The behaviour in this PR means that abort reactions in userland and the web platform interleave. @bakkot is keen on this. I'm a little unsure, so I'm interested in more opinions.

I'm a little worried about:

const controller = new AbortController();
const signal = controller.signal;

let thing;

signal.addAbortCallback(() => {
  console.log(thing.running);
  // The above is unexpectedly true,
  // because the platform's steps to set it to false haven't run yet.
});

thing = doThing(signal);
controller.abort();

But maybe this already happens with promises?


  • At least two implementers are interested (and none opposed):
  • Tests are written and can be reviewed and commented upon at:
  • Implementation bugs are filed:
    • Chromium: …
    • Gecko: …
    • WebKit: …
    • Deno (only for aborting and events): …
    • Node.js (only for aborting and events): …
  • MDN issue is filed: …
  • The top of this comment includes a clear commit message to use.

(See WHATWG Working Mode: Changes for more details.)


Preview | Diff

@bakkot
Copy link

bakkot commented Nov 20, 2025

With regards to the example, the way a user is actually likely to run into this is more like

import { operation } from 'some-framework';

async function whatever(signal) {
  await Promise.all([
    builtin1({ signal }),
    operation({ signal }),
    builtin2({ signal }),
  ]);
}

From the user's perspective, it is very strange if these are aborted in anything other than LIFO or FIFO order. Users are not (and should not be) reasoning about whether the operations they're calling are defined in their framework or in the platform, and there is no way to understand the builtin1, builtin2, operation order without such reasoning.

@annevk annevk added needs implementer interest Moving the issue forward requires implementers to express interest addition/proposal New features or enhancements topic: aborting AbortController and AbortSignal labels Nov 20, 2025
@jakearchibald
Copy link
Collaborator Author

That example is pretty compelling.

@saschanaz
Copy link
Member

I'd like the design to allow #1389 in the future 👀 (doesn't seem to block it right now, which is cool)

@jakearchibald jakearchibald added the agenda+ To be discussed at a triage meeting label Nov 26, 2025
@jasnell
Copy link
Contributor

jasnell commented Dec 4, 2025

From a language-level perspective, I think it's better if we defined the mechanism as a cancelation/abort protocol in the language... of which AbortSignal could be update to be / considered an implementation.

Specifically, I've been imagining something along the lines of...

const genericAbortSignal = {
  [Symbol.onabort]() {
    // Register and return the abort handle
    return {
      cancel() { /* Unregister the abort handler */ },
      [Symbol.dispose]() { this.cancel(); }
    };
  },
};

doSomething(genericAbortSignal);

async function doSomething(signal) {
  using abortHandle = signal[Symbol.onabort]();
  abortHandle.onabort = (reason) => { /* ... */ };
  // Do async stuff that can be aborted...
}

Then, the actual AbortSignal class here could just add [Symbol.addAbortHandler] such that it defers to addEventListener.

This type of approach would allow any object to become an AbortSignal. It would allow TC39 to define abort semantic without relying on AbortSignal, AbortController, Event, or EventTarget.

The requirement of the protocol would be that a call to Symbol.onabort must return an object that minimally must expose a cancel() method that would unregister the handler from the signal.

AbortSignal could easily be adapted to support this:

class AbortSignal extends EventTarget {
  // ...

  [Symbol.onabort]() {
    let callback;
    const self = this;
    const handler = {
      cancel() {
        self.removeEventListener('abort', callback);
      };
    };
    callback = (event) => handler.onabort?.(event.reason);
    this.addEventListener('abort', callback, { once: true });
    return handler;
  }
}

@bakkot
Copy link

bakkot commented Dec 4, 2025

@jasnell There's no need to define a symbol-based protocol. The protocol for consumers can literally be "call .addAbortCallback(yourCleanupFunction)". That requires no other changes from WHATWG.

(Though I agree that a removeAbortCallback(yourCleanupFunction), or similar mechanism, would also be nice.)


Edit: also, your implementation ends up still using the addEventListener; the primary purpose of this PR is to give us a path to avoid that machinery, because of the various problems with it.

@jasnell
Copy link
Contributor

jasnell commented Dec 5, 2025

... also, your implementation ends up still using the addEventListener

Only within a hypothetical impl of the abort protocol in the existing AbortSignal machinery. That would not be an actual requirement tho. I showed that only for illustration.

I think the nice thing about protocol approach is that it would not require use of the AbortSignal class at all. From the languages point of view, an "abort signal" can be any object that happens to implement @@Symbol.onabort (or whatever we want to call it). Exactly how that is implemented is left entirely open.

@bakkot
Copy link

bakkot commented Dec 5, 2025

Calling .addAbortCallback(yourCleanupFunction) works just as well as using a symbol; arbitrary objects can have a method named "addAbortCallback". There's no need to introduce a new symbol here.

The main reasons to use a symbol-based rather than string-based protocol are when this is a protocol that arbitrary objects might implement (like Iterator), or when you want to switch on the presence of the protocol (like Promise). Neither holds here.

@jasnell
Copy link
Contributor

jasnell commented Dec 5, 2025

Symbol vs Not-a-symbol isn't that important to me. I'm good with either. More focused on the overall pattern than the particular color of the bikeshed ;-)

@bakkot
Copy link

bakkot commented Dec 5, 2025

So the only difference between what you're proposing and what's in this PR already is just that instead of the new method returning undefined it would return an object with a cancel method which removes the listener?

@jasnell
Copy link
Contributor

jasnell commented Dec 5, 2025

That and we'd be sure to define it (in the language) as a protocol that is not dependent specifically on using AbortSignal/AbortController. I want to make certain that it is allowable that !(signal instanceof AbortSignal but it would still work...

that is, I want us to say, "An abort signal is any object that implements [...] function" ... as opposed to "An abort signal is always an AbortSignal as defined by WHATWG"... hopefully that make sense.

@bakkot
Copy link

bakkot commented Dec 5, 2025

Yes, the idea is that ECMAScript would literally use the new method added in this PR (i.e., it will just attempt to call addAbortCallback on whatever object is passed).

If ECMAScript was going to reach into the internals of AbortSignal there would be no need for the new method.

<li>
<p>[=AbortSignal/Add|Add the following abort steps=] to [=this=]:</p>
<ol>
<li><p><a spec=webidl>invoke</a> <var>callback</var> with « », and "<code>report</code>".
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noob question: is it implicit that the callback is no longer held once it's called?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The abort algorithms list is cleared when abort signals are run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

addition/proposal New features or enhancements agenda+ To be discussed at a triage meeting needs implementer interest Moving the issue forward requires implementers to express interest topic: aborting AbortController and AbortSignal

Development

Successfully merging this pull request may close these issues.

8 participants