AbortController: Patterns Beyond Fetch
AbortController isn't just for fetch—use it to cancel event listeners, timeouts, and any async operation with a single abort() call.
Most developers know AbortController for cancelling fetch requests.
But it's a general-purpose cancellation primitive—perfect for event listeners,
timeouts, and any async operation. One abort() cleans up everything
sharing that signal.
The Classic: Fetch Cancellation
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then(res => res.json())
.catch(err => {
if (err.name === 'AbortError') {
console.log('Request cancelled');
}
});
// Later: cancel the request
controller.abort();
This is well-known. But AbortController does much more.
Event Listener Cleanup
addEventListener accepts a signal option. When aborted,
the listener is automatically removed—no need to keep a reference to the handler:
const controller = new AbortController();
// Add multiple listeners with one signal
window.addEventListener('resize', handleResize, { signal: controller.signal });
window.addEventListener('scroll', handleScroll, { signal: controller.signal });
document.addEventListener('keydown', handleKey, { signal: controller.signal });
// One abort() removes all three
controller.abort();
No more storing handler references just to call removeEventListener.
This is especially clean in React's useEffect:
useEffect(() => {
const controller = new AbortController();
const { signal } = controller;
window.addEventListener('keydown', (e) => {
if (e.key === 'Escape') onClose();
}, { signal });
window.addEventListener('resize', recalculate, { signal });
return () => controller.abort();
}, [onClose]);
The cleanup function is a single line. The inline handler works because
you don't need to pass the same function reference to removeEventListener.
Timeout Cancellation
Link setTimeout and setInterval to an abort signal:
function cancellableTimeout(ms, signal) {
return new Promise((resolve, reject) => {
const id = setTimeout(resolve, ms);
signal?.addEventListener('abort', () => {
clearTimeout(id);
reject(new DOMException('Timeout aborted', 'AbortError'));
});
});
}
const controller = new AbortController();
cancellableTimeout(5000, controller.signal)
.then(() => console.log('Timer completed'))
.catch(err => {
if (err.name === 'AbortError') console.log('Timer cancelled');
});
// Cancel before 5s
controller.abort();Composing Multiple Operations
Use one controller to coordinate multiple async operations:
async function loadDashboard(signal) {
const [users, posts, analytics] = await Promise.all([
fetch('/api/users', { signal }),
fetch('/api/posts', { signal }),
fetch('/api/analytics', { signal }),
]);
// If any request fails due to abort, all are cancelled
return { users, posts, analytics };
}
const controller = new AbortController();
loadDashboard(controller.signal);
// User navigates away—cancel everything
controller.abort();AbortSignal.timeout()
Create a signal that auto-aborts after a duration—built into the platform:
// Abort after 5 seconds
const signal = AbortSignal.timeout(5000);
fetch('/api/slow-endpoint', { signal })
.catch(err => {
if (err.name === 'TimeoutError') {
console.log('Request timed out');
}
});
Note: timeout signals throw TimeoutError, not AbortError.
AbortSignal.any()
Combine multiple signals—abort when any of them fires:
const userController = new AbortController();
const timeoutSignal = AbortSignal.timeout(10000);
const combinedSignal = AbortSignal.any([
userController.signal,
timeoutSignal,
]);
fetch('/api/data', { signal: combinedSignal });
// Cancels if: user aborts OR 10 seconds passReact Pattern: Race Condition Prevention
A common bug: rapid state changes cause stale responses to overwrite fresh ones. AbortController fixes this elegantly:
function useSearch(query) {
const [results, setResults] = useState([]);
const controllerRef = useRef(null);
useEffect(() => {
// Cancel previous request
controllerRef.current?.abort();
controllerRef.current = new AbortController();
fetch(`/api/search?q=${query}`, {
signal: controllerRef.current.signal,
})
.then(res => res.json())
.then(setResults)
.catch(err => {
if (err.name !== 'AbortError') throw err;
});
return () => controllerRef.current?.abort();
}, [query]);
return results;
}Each query change aborts the previous request. No race conditions, no stale data.
Custom Async APIs
Make any async API cancellable by accepting a signal:
async function processLargeDataset(data, signal) {
const results = [];
for (const item of data) {
// Check for abort between iterations
if (signal?.aborted) {
throw new DOMException('Processing cancelled', 'AbortError');
}
results.push(await processItem(item));
}
return results;
}
const controller = new AbortController();
processLargeDataset(bigArray, controller.signal);Key Takeaways
addEventListeneraccepts asignaloption—oneabort()removes all listeners- Use
AbortSignal.timeout()for auto-cancelling after a duration - Use
AbortSignal.any()to combine multiple abort conditions - In React
useEffect, abort in the cleanup function for clean resource management - Pass signals through your async APIs to make them cancellable
Advertisement
Explore these curated resources to deepen your understanding
Official Documentation
Related Insights
Explore related edge cases and patterns
Advertisement