How to use AbortController as a general, standard way to abort async tasks

Cameron Nokes
Aug 26 · 3 min read

Originally published at https://cameronnokes.com/blog/cancelling-async-tasks-with-abortcontroller/

I learned the other day that AbortController can be used to not only abort fetches, but can be used in basically any way you like. Just like promises can be used to represent any future, pending value, AbortController can be used as a controller to stop pending async operations. This was exciting to me, which I realize probably comes off sad sounding and means that I need more excitement in my life but--whatever--that's besides the point.

The typical usage of AbortController that I was aware of was using it with fetch like this:

let abortController = new AbortController();
fetch('/url', { signal: abortController.signal });
abortController.abort();

Calling the abort method cancels the HTTP request and rejects the promise. How exactly an AbortController.signal is consumed wasn't clear to me but it turns out it's pretty simple.

AbortControllerSignals implement the DOM's EventTarget interface, or in other words, are an event emitter with an addEventListener method. This allows us to use it like this:

let abortController = new AbortController(); abortController.signal.addEventListener('abort', (event) => {
// implement your canceling logic here
});

So let’s write a function that does an async task that allows the caller to pass an AbortControllerSignal to it to cancel it. I've occasionally had reason to create a function that basically promisifies setTimeout in actual code, so we'll use that as our example:

Why does this matter?

I think there’s a lot of value in using standardized patterns. For example, Promises gave us a standard way to represent future values. There are alternatives to native Promises, some of those approaches are arguably better, but they’re not standard. Any time you consume code that uses non-standard approaches, there’s some conflict. How do I integrate this into existing code that uses different patterns? Do other people on my team need to learn new things now to properly leverage it? Or if you’re writing a library, there’s the dependency cost to consider. That’s why the idea of a standard way of aborting an asynchronous operation that’s baked into the platform seems valuable to me. Currently AbortController is a DOM only thing, but there's a proposal to bring fetch into Node.js, which would probably mean bringing it over there as well.

What do you do when the async task can’t be aborted?

Not all async operations are abortable, for example, Notification.requestPermission(), and promises don't have any sort of way to unsubscribe callbacks you've passed to then or catch (I'm not even sure that they should). This could be problematic in a use case like this that I made up:

function requestNotificationPermission(opts = {}) { 
return fetch('/user/settings', { signal: opts.signal })
.then(res => res.json())
.then(({ canNotify }) => {
if (canNotify) {
return Notification. requestPermission();
} else {
throw new Error()
}
});
}
let abortController = new AbortController();requestNotificationPermission({ signal: abortController.signal })
.then(/**/)
.catch(/**/);
// whenever and where ever this is called, I should be able to expect that
// the promise above will be rejected... but that's not the case
abortController.abort();

In the above example, the abort() will cancel the fetch if it's in-flight, but if it's already resolved, then the promise will resolve and go on to the next call, Notification.requestPermission, where there's no way of canceling it. So the promise returned by requestNotificationPermission might resolve even though I've aborted it. Just depends on where you are in that series of async tasks. So how do we fix that?

For this use case, this is the best I can think of:

function requestNotificationPermission(opts = {}) { 
let isAborted = false;
opts.signal.addEventListener('abort', () => {
isAborted = true;
});
return fetch('/user/settings', { signal: opts.signal })
.then(res => res.json())
.then(({ canNotify }) => {
if (canNotify) {
return Notification.requestPermission();
} else {
throw new Error();
}
})
.then(result => {
if (isAborted) {
throw new Error();
} else {
return result;
}
});
}

Conclusion

While the using AbortController is a bit clunky, I appreciate that it provides a standardized, composable solution to cancel async tasks.

Cameron Nokes

Javascript and front-end development how-tos

Cameron Nokes

Written by

Front-end developer and hamburger eater. Check out my screencasts: https://egghead.io/instructors/cameron-nokes

Cameron Nokes

Javascript and front-end development how-tos

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade