AbortController & AbortSignal: Cascading Fetch/Listener Cleanup
- Last updated
- Prerequisites:
- fetch and async/await basics
- Understanding of event listeners and cleanup
- abort-controller
- abort-signal
- fetch
- cleanup
- cancellation
- events
Read at your depth
The practical view
AbortController lets you cancel fetch requests and other async work. const ac = new AbortController(); fetch(url, { signal: ac.signal }); // later: ac.abort();. Aborting rejects the fetch with an AbortError (name === 'AbortError'), and you catch it to distinguish cancellation from real failures. The signal is also an EventTarget: signal.addEventListener('abort', handler) lets other work react. You can pass the same signal to multiple fetches, so one abort cancels them all. Always clean up: remove the listener or call controller.abort() in teardown to avoid leaks in components that unmount mid-request.
The same idea in other frameworks
React effects commonly create an AbortController per effect and abort() in cleanup to cancel in-flight fetches on unmount.Angular's HttpClient supports { signal } for request cancellation; subscriptions also need manual unsubscribe, but signals cover fetch-style work.Vue composables (onScopeDispose) pair with AbortController for cancellation when the component scope tears down.Svelte 5 $effect cleanup can abort controllers automatically, and SvelteKit load functions receive fetch that can be signaled.Legacy vs modern
Ignoring cancellation vs aborting in teardown
An unmanaged fetch continues after unmount and can set state on a dead component; aborting in cleanup cancels the request and prevents state races.
fetch(url).then(setState); // ❌ OLD: no signal — keeps running after unmount, may setState on a removed componentconst ac = new AbortController();
const res = await fetch(url, { signal: ac.signal }); // ✅ NEW: wired in
ac.abort(); // cancels the request; rejects with AbortErrorInterview gotchas
Context
This probes the cancellation mechanics — that abort rejects with a specific error type and triggers network teardown — which is central to correct async UI code.
The mechanical answer
Calling abort() sets the signal's aborted flag, stores the abort reason, and synchronously dispatches the abort event. The fetch operation observes the signal; its promise rejects with a DOMException whose name is 'AbortError' (the stored reason) rather than a network/HTTP error. The underlying network request is torn down — the connection is cancelled and any streaming response is aborted. To distinguish cancellation from failures, check err.name === 'AbortError' (or err === signal.reason) and rethrow otherwise. Because the rejection is a DOMException, unhandled abort rejections still surface as unhandled rejections if you forget the catch.
Trap
The naive answer is 'abort just makes fetch return undefined'. It rejects the promise, and uncaught abort rejections cause unhandled-rejection errors. Another trap: catching the error and treating AbortError like a server error — cancellation is expected control flow, not a failure, so it must be filtered. Candidates who check the error name and rethrow non-abort errors show production-ready async handling.
Context
This tests listener hygiene and fan-out semantics — interviewers want to see that AbortSignal is an EventTarget whose listeners must be cleaned up, not just a fetch option.
The mechanical answer
AbortSignal is an EventTarget: registering signal.addEventListener('abort', fn) means `fn` is retained as long as the signal lives AND the listener isn't removed. If you keep the controller in a long-lived module/scope, stale listeners accumulate — each holding its closure — which leaks memory and can run unexpected code on later aborts. The fan-out pattern is safe: passing one signal to many fetches (or streams/event listeners) makes abort() cancel all of them in the same synchronous dispatch. For multi-source cancellation, AbortSignal.any([...]) creates a dependent signal that propagates when any source aborts; dependent signals internally register with their sources, so removing listeners on the dependent still matters for cleanup.
Trap
The naive answer is 'abort listeners are automatically removed when the fetch completes'. They are not — a completed fetch does not unsubscribe custom abort listeners. Another trap: assuming aborting a controller with no active listeners is a no-op you can skip cleaning up; if you registered any listener, you must remove it or null the controller reference. Candidates who treat signal listeners with the same hygiene as addEventListener show real discipline.