AbortController and cancellation
One signal cancels them all — fetch, listeners, observers, timers.
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
AbortControllerholds the on/off switch. It has one method,controller.abort(). - An
AbortSignalis the read-only side. It lives atcontroller.signaland 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).
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.
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".
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.
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.
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.
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.
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.
Try it yourself
signal.aborted tell you?{ signal }. The user closes the modal. What's the cleanest way to remove all six?