Cancelling Fetch Requests with AbortController in JavaScript
Why cancellation matters
When a user types in an autocomplete box, each keystroke can trigger a network request. If the user continues typing, earlier requests become stale and waste bandwidth. Without a way to abort them, responses may arrive out of order and corrupt the UI.
AbortController gives you a first‑class signal that any fetch can listen to. It's built into the platform, requires no extra dependencies, and works in every modern browser.
Real‑world scenario: search autocomplete
Imagine a component that calls /api/search?q=term on every input event. The user types "java", pauses, then adds "script" within 200 ms. Two requests fire. The first returns after the second, overwriting the correct suggestions with results for "java" only.
By attaching an AbortSignal to each fetch and aborting the previous one on the next keystroke, only the latest query can update the list.
Implementation
// Utility that wraps fetch with automatic cancellation
function createSearchFetcher() {
let controller = null;
return async function search(query) {
// Abort any in‑flight request
if (controller) {
controller.abort();
}
// Fresh controller for this call
controller = new AbortController();
const signal = controller.signal;
try {
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
signal // attach signal so fetch reacts to abort()
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (err) {
// Ignore AbortError – it means we deliberately cancelled
if (err.name === 'AbortError') return null;
throw err; // re‑throw genuine network errors
}
};
}
// Usage in an input handler
const search = createSearchFetcher();
const input = document.getElementById('search-input');
const results = document.getElementById('results');
input.addEventListener('input', async (e) => {
const term = e.target.value.trim();
if (!term) {
results.innerHTML = '';
return;
}
const data = await search(term);
if (!data) return; // request was aborted
results.innerHTML = data.map(item => `${item.label} `).join('');
});
What the code does
- createSearchFetcher returns a closure that holds the latest
AbortController. Each call aborts the previous controller before creating a new one. - The
signaloption is passed tofetch. Whencontroller.abort()runs, the fetch promise rejects with anAbortError. - The catch block distinguishes
AbortErrorfrom real failures. Returningnulllets the caller ignore cancelled requests. - The event listener debounces naturally because the previous request is killed before the next one starts.
Why not just debounce?
Debouncing delays the request, which improves perceived latency but doesn't solve the out‑of‑order problem if the user types faster than the debounce window. Cancellation guarantees that only the freshest request can mutate state.
Edge cases & tips
- Multiple concurrent fetch types – keep a map of controllers keyed by request type (e.g., search, suggestions, analytics) instead of a single variable.
- Server‑side support – aborting the client side stops the download, but the server may still process the request. That's usually fine for read‑only endpoints.
- Cleanup on unmount – if you use this in a framework component, call
controller.abort()in the cleanup phase to avoid memory leaks.
AbortController is a tiny API with huge impact. Once you start using it, you'll wonder how you lived without deterministic request lifecycles.
Takeaway
Wrap fetch in a small factory that owns an AbortController. Abort on every new call, handle AbortError gracefully, and you get race‑free data fetching with virtually no boilerplate. It's a pattern that scales from a simple autocomplete to complex dashboards with dozens of simultaneous queries.