The idea of an “abortable” fetch came to life in 2017 when AbortController
was released. That gives us a way to bail on an API request initiated by fetch()
— even multiple calls — whenever we want.
Here’s a super simple example using AbortController
to cancel a fetch()
request:
const controller = new AbortController();
const res = fetch('/', { signal: controller.signal });
controller.abort();
console.log(res); // => Promise(rejected): "DOMException: The user aborted a request"
You can really see its value when used for a modern interface of setTimeout
. This way, making a fetch timeout after, say 10 seconds, is pretty straightforward:
function timeout(duration, signal) {
return new Promise((resolve, reject) => {
const handle = setTimeout(resolve, duration);
signal?.addEventListener('abort', e => {
clearTimeout(handle);
reject(new Error('aborted'));
});
});
}
// Usage
const controller = new AbortController();
const promise = timeout(10000, controller.signal);
controller.abort();
console.log(promise); // => Promise(rejected): "Error: aborted"
But the big news is that addEventListener
now accepts an Abort Signal as of Chrome 88. What’s cool about that? It can be used as an alternate of removeEventListener
:
const controller = new AbortController();
eventTarget.addEventListener('event-type', handler, { signal: controller.signal });
controller.abort();
What’s even cooler than that? Well, because AbortController
is capable of aborting multiple cancelable requests at once, it streamlines the process of removing multiple listeners in one fell swoop. I’ve already found it particularly useful for drag and drop.
Here’s how I would have written a drag and drop script without AbortController
, relying two removeEventListener
instances to wipe out two different events:
// With removeEventListener
el.addEventListener('mousedown', e => {
if (e.buttons !== 1) return;
const onMousemove = e => {
if (e.buttons !== 1) return;
/* work */
}
const onMouseup = e => {
if (e.buttons & 1) return;
window.removeEventListener('mousemove', onMousemove);
window.removeEventListener('mouseup', onMouseup);
}
window.addEventListener('mousemove', onMousemove);
window.addEventListener('mouseup', onMouseup); // Can’t use `once: true` here because we want to remove the event only when primary button is up
});
With the latest update, addEventListener
accepts the signal
property as its second argument, allowing us to call abort()
once to stop all event listeners when they’re no longer needed:
// With AbortController
el.addEventListener('mousedown', e => {
if (e.buttons !== 1) return;
const controller = new AbortController();
window.addEventListener('mousemove', e => {
if (e.buttons !== 1) return;
/* work */
}, { signal: controller.signal });
window.addEventListener('mouseup', e => {
if (e.buttons & 1) return;
controller.abort();
}, { signal: controller.signal });
});
Again, Chrome 88 is currently the only place where addEventListener
officially accepts an AbortSignal. While other major browsers, including Firefox and Safari, support AbortController
, integrating its signal with addEventListener
is a no go at the moment… and there are no signals (pun sorta intended) that they plan to work on it. That said, a polyfill is available.
Hey, this is really cool!, thanks for the post!
In the last JS example, shouldn’t
abortController.signal
becontroller.signal
?, because there isn’t any variable calledabortController
Fixed. Thanks.
Yeah, It doesn’t work.
If you look at the platform bug tickets listed in https://github.com/whatwg/dom/pull/919, it actually looks like Firefox and Safari have both already implemented this.
Will the event handler function get garbage collected after we call
controller.abort()
?If not, it seems that using AbortController hurts performance.
It seems the feature is behind a flag in Chrome 88 and will only be enabled by default in Chrome 90. As per signals from other browsers, this issue suggests the feature is planned for the Firefox 86 release.
…and the corresponding Webkit issue has been resolved, so I’m confident we’ll have good support soon!
chromestatus.com is not a great place to track other vendors’ statuses, as they don’t track them automatically and they don’t update that part often…
Signals in event listeners are now supported on Firefox, but iOS and desktop Safari both still lack support.
https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#browser_compatibility
We checked today if this is now working in all Browsers and I can confirm that Safari 15 and Firefox 101 also support this.
If you have to support Safari 14 you can use this tiny Polyfill.
https://github.com/nuxodin/lazyfill/blob/main/monkeyPatches/addEventListenerSignal.js