Me

Bart Słysz

Software Engineer

AbortController vs AbortSignal as spidermans
generated by gemini.google.com

AbortController is actually cooler than you think

A personal story: I don't recall an exact moment when I saw a use of this tool for the first time, but can clearly remember I had strong negative bias against it.

That's probably because I had been already working with a lot of so-called enthusiasts, who were always desperately trying to find a use case for the new thingy they've just explored (and not gonna lie, very often that's me 😬).

Now that I've been using it for a while, I can tell that it's one of the best tools I discovered in a long time. To say it loudly and proudly: this tool is badass awesome!

A nightmare called .unsubscribe()

Using event listeners is a regular thing in the JS world. If you've ever read any tutorials or documentation about using them along with React, you've probably seen a disclaimer like this:

remember to unsubscribe from the event when you don't need it anymore, otherwise you'll (blah blah blah)

And the very first approach looks as follows:

useEffect(() => {
  // caveat #1: function name
  const handleClick = () => {
    console.log('clicked');
  };

  // caveat #2: function reference
  window.addEventListener('click', handleClick);

  return () => {
    window.removeEventListener('click', handleClick);
  };
}, []);

There are two caveats in the code above:

  • #1: I'm literally about to cry when I see functions named like this. Really. The structure of the code enforces us to come up with some names, giving us room to name them meaningfully and reduce cognitive load. And yet, developers often don't take advantage of that and oftentimes use something I can't live with: onClick={handleClick} 😭😭😭

    Come on, you can do better than that!

  • #2: before fancy AI-powered code review tools came along, it was pretty common to forget that .removeEventListener() needs to take the same function reference as the one used in .addEventListener()

    If you were lucky, you could notice duplicated behaviors or other quirks (particularly when using React>=18 with StrictMode). I committed this crime quite a few times, but well... I can easily blame it on the fact it's not super developer friendly.

Speaking of developer friendliness

Well, to a certain extent we can tell, that code is developer friendly when a developer can easily understand it. But that's subjective: your code will be understood better by you than by anyone else (until they're forced to get familiar with it).

Objectively, we can say that code is developer friendly, when its architecture minimizes the risk of making similar mistakes like the one above with .removeEventListener(). And surprise, surprise -- that's what AbortController does!

useEffect(() => {
  const controller = new AbortController();

  window.addEventListener('click', () => {
    // neither of the previous caveats actually exist
  }, { signal: controller.signal });

  return () => {
    controller.abort();
  };
}, []);

I really like thinking about AbortController like it's a remote controller. You press a button and your device turns off. No need to remember about function reference, no need to wonder if you still need to fight an internal battle with onClick={handleClick}. It's really that simple!

And what I like most is that AbortController is widely used. The very first impression I got about this was wrong -- I thought it was a solution for a single use-case, but it turns out it's more widely supported than you'd expect. You can use it with FetchAPI, DOM, WebSocket and many others. Really appreciate ECMA for standardizing APIs like this.

I owe you an apology... or maybe not?

So far I talked about AbortController and showed a lot of use cases, but... it's not the AbortController that is cool, it's the AbortSignal.

As I said before, AbortController is like a remote controller, one of the things it can do is to send a signal to the device to turn off. You can notice in the code block above, that it's abortController.signal we use with event listeners, and AbortController is just a method to create such signal and trigger abort if needed.

Is there anything then, that could generate such a signal without doing it manually through controller? Well... yes!

AbortSignal.timeout()

I showed you the name and you immediately get it, didn't you? The code below is just a formality:

fetch(url, { signal: AbortSignal.timeout(5_000) })
  .catch((error) => {
    if (error.name === 'TimeoutError') {
      // oof, we've got a timeout
    }
  })

AbortSignal.any()

We can envision two people holding their remote controllers, and one of them is pressing the button. In that case we need to listen to both of them and this is what AbortSignal.any() does:

const controller1 = new AbortController();
const controller2 = new AbortController();

const combinedSignal = AbortSignal.any([
  controller1.signal,
  controller2.signal,
]);

combinedSignal.addEventListener('abort', () => {
  const aborter = controller1.signal.aborted ? 2 : 1;
  console.log(`Turned off by controller${aborter}`);
}, { once: true });

controller2.abort();
controller1.abort();

// the only visible output will be: Turned off by controller2

Summary

  1. If you can use a technique that minimizes the risk of making even obvious mistakes, use it -- that compounds and in longer term distinguishes good codebases from the bad ones

    AbortSignal is definitely a technique that minimizes that risk.

  2. You should really avoid using statements like onClick={handleClick} that only clutter your code and take away the chance of making things more clear

    When it comes to naming, call a spade a spade (not handleSpade).

  3. Bias is a real thing, even in the world of programming. Don't let yourself stop exploring new tools just because you don't have a good experience with them (or have no experience whatsoever)

    Be pragmatic and open-minded to new solutions.