Web Platform APIs · 2 / 10
lesson 2

AbortController and cancellation

One signal cancels them all — fetch, listeners, observers, timers.

~ 13 min read·lesson 2 of 10
0 / 10

You're building a search box. Every keystroke kicks off a fetch to the server. The user types react, then quickly changes to redux. Three requests are now in flight, and they may come back out of order — the slow result for r might land after the fast one for redux, overwriting the right answer with a stale one. The fix isn't a bigger debounce. It's cancelling the requests you no longer want.

AbortController is the browser's standard way to say "never mind". One controller can cancel a fetch, an event listener, an observer, even a timer. Once you see the shape, you'll spot the same pattern across half the platform.

Controller and signal

The model is two objects with one job between them:

  • An AbortController holds the on/off switch. It has one method, controller.abort().
  • An AbortSignal is the read-only side. It lives at controller.signal and is what you hand to other APIs.

You give APIs the signal. You keep the controller and call abort() when you've changed your mind. Think of it as a remote control (you keep this) and a TV (everything you handed the signal to, all listening for the off button).

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

console.log(signal.aborted);   /* false */
controller.abort();
console.log(signal.aborted);   /* true */

signal.aborted flips from false to true after abort(). The signal also fires an "abort" event, which is how everything else finds out.

Tip

A signal is one-shot. Once aborted, it stays aborted — you can't reset it. If you need to cancel a new round of work, make a new AbortController.

Cancelling a fetch

Pass the signal in the fetch options. When you call controller.abort(), the fetch's promise rejects with a DOMException whose name is "AbortError".

search.js
let active = null;  /* the current controller, or null */

async function search(query) {
active?.abort();                       /* cancel the previous one */
active = new AbortController();
try {
  const response = await fetch(`/api/search?q=${query}`, {
    signal: active.signal,
  });
  return response.json();
} catch (err) {
  if (err.name === "AbortError") return;   /* expected — ignore */
  throw err;
}
}

The pattern: keep a slot for the current controller, abort it before starting the next request, store the new one. Each new search aborts the previous, so out-of-order results stop being a problem — the stale ones never resolve.

The if (err.name === "AbortError") return is important. Cancellation is not a failure; it's just a thing you asked for. Treat it like an empty result, not an exception worth logging.

check your understanding
Your search-box code aborts the previous fetch on each keystroke. Why does the cancellation appear in your error logs as a noisy "AbortError"?

Cancelling event listeners

The same signal works as a removeEventListener shortcut. Pass it in the third argument, and when the controller aborts, the listener is removed automatically.

dropdown.js
function openDropdown() {
const controller = new AbortController();
const { signal } = controller;

document.addEventListener("click", onClickAway, { signal });
document.addEventListener("keydown", onEscape, { signal });

return () => controller.abort();   /* close: removes BOTH listeners */
}

Without AbortController, you'd have to keep references to each handler and call removeEventListener for each one. With the signal, one abort() removes every listener that was registered with it. This is how modern code keeps cleanup tidy — you accumulate listeners under one controller, then drop them all at once.

controllerfetch click keydown timer
One controller, many listeners — abort() removes them all in one shot.
Tip

When a component or modal opens, create an AbortController, pass signal to every listener, fetch, and observer it sets up, and call abort() when it closes. One line of cleanup, no leaks.

Built-in timeout signal

Often you don't need a controller at all — you just want "give up after N seconds". AbortSignal.timeout(ms) returns a signal that aborts itself after that delay.

timeout.js
const response = await fetch("/api/slow", {
signal: AbortSignal.timeout(5000),   /* aborts after 5 seconds */
});

If the request hasn't completed in 5 seconds, the signal aborts and the fetch rejects with a TimeoutError (still a DOMException, just a different name). You don't have to wire up setTimeout and controller.abort() yourself — the browser does it for you.

There's also AbortSignal.any([signalA, signalB]), which gives you a signal that aborts as soon as either of the inputs aborts. You'd use it to combine a per-component cancel signal with a global "user navigated away" signal.

check your understanding
You want a fetch that times out after 3 seconds OR cancels when the user closes a panel (which has its own controller). Which approach gets both behaviors?

Try it yourself

check your understanding
A user types into a search box. You want each new keystroke to cancel any in-flight request from prior keystrokes. What's the right shape?
check your understanding
What does signal.aborted tell you?
check your understanding
You set up six event listeners during a modal's lifetime, all with the same { signal }. The user closes the modal. What's the cleanest way to remove all six?
← prevnext lesson →
KeepLearningcertificate
for completing
Web Platform APIs
0 of 10 read