Canceling Fetch Requests in JavaScript Full Guide
A complete guide to canceling Fetch requests in JavaScript. Covers AbortController fundamentals for Fetch, canceling on user navigation, race condition prevention in search inputs, canceling parallel requests, implementing request timeouts, building a FetchManager class for lifecycle tracking, and cleanup patterns for single-page applications.
The Fetch API does not have a built-in cancel method. Instead, you pass an AbortSignal to the fetch() call and trigger it via the associated AbortController. This guide focuses specifically on Fetch cancellation patterns: preventing stale responses, canceling on navigation, race condition elimination, timeout implementation, and lifecycle management.
Why Cancel Fetch Requests?
| Scenario | Problem Without Cancel | Solution |
|---|---|---|
| User types in search | Stale results overwrite current results | Cancel previous request |
| User navigates away | Wasted bandwidth, potential state updates on unmounted component | Cancel on route change |
| Request takes too long | User waits indefinitely | Cancel after timeout |
| Component unmount | Memory leak, setState on unmounted component | Cancel in cleanup |
| Duplicate submissions | Double form post, duplicate data | Cancel previous, keep latest |
Basic Cancel Pattern
const controller = new AbortController();
fetch("/api/users", { signal: controller.signal })
.then((response) => response.json())
.then((data) => console.log(data))
.catch((error) => {
if (error.name === "AbortError") {
console.log("Fetch was canceled");
} else {
console.error("Fetch failed:", error);
}
});
// Cancel the request
controller.abort();When abort() is called, the Fetch promise rejects with a DOMException where name is "AbortError".
Cancel on User Navigation
class PageController {
constructor() {
this.controller = null;
}
async loadPage(url) {
// Cancel any pending request from the previous page
if (this.controller) {
this.controller.abort();
}
this.controller = new AbortController();
try {
const response = await fetch(url, { signal: this.controller.signal });
const html = await response.text();
document.getElementById("content").innerHTML = html;
} catch (error) {
if (error.name === "AbortError") return; // Navigated away, ignore
document.getElementById("content").innerHTML = "<p>Failed to load page</p>";
}
}
destroy() {
if (this.controller) this.controller.abort();
}
}
const page = new PageController();
// SPA navigation
document.querySelectorAll("nav a").forEach((link) => {
link.addEventListener("click", (e) => {
e.preventDefault();
page.loadPage(link.href);
});
});Preventing Race Conditions in Search
The classic race condition: user types "cat", then "car". The "car" request completes before "cat", but then the slower "cat" response arrives and overwrites the correct "car" results.
class SearchController {
constructor(resultsContainer) {
this.container = resultsContainer;
this.controller = null;
this.currentQuery = "";
}
async search(query) {
// Cancel any in-flight search
if (this.controller) {
this.controller.abort();
}
this.controller = new AbortController();
this.currentQuery = query;
if (!query.trim()) {
this.container.innerHTML = "";
return;
}
try {
const response = await fetch(
`/api/search?q=${encodeURIComponent(query)}`,
{ signal: this.controller.signal }
);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
// Double-check: only render if this is still the current query
if (query === this.currentQuery) {
this.renderResults(data.results);
}
} catch (error) {
if (error.name === "AbortError") return;
this.container.innerHTML = "<p>Search failed</p>";
}
}
renderResults(results) {
this.container.innerHTML = results
.map((r) => `<div class="result"><h3>${r.title}</h3><p>${r.excerpt}</p></div>`)
.join("");
}
cancel() {
if (this.controller) this.controller.abort();
}
}Canceling Parallel Requests
async function loadDashboardData(signal) {
const endpoints = [
"/api/user/profile",
"/api/user/projects",
"/api/notifications",
"/api/analytics",
];
const requests = endpoints.map((url) =>
fetch(url, { signal }).then((r) => r.json())
);
try {
const results = await Promise.allSettled(requests);
return {
profile: results[0].status === "fulfilled" ? results[0].value : null,
projects: results[1].status === "fulfilled" ? results[1].value : [],
notifications: results[2].status === "fulfilled" ? results[2].value : [],
analytics: results[3].status === "fulfilled" ? results[3].value : null,
};
} catch (error) {
if (error.name === "AbortError") {
console.log("All dashboard requests canceled");
return null;
}
throw error;
}
}
// Usage
const controller = new AbortController();
const data = loadDashboardData(controller.signal);
// Cancel everything at once
controller.abort();Fetch With Timeout
async function fetchWithTimeout(url, options = {}) {
const { timeout = 10000, ...fetchOptions } = options;
const controller = new AbortController();
// If user passed a signal, combine with timeout
if (fetchOptions.signal) {
fetchOptions.signal.addEventListener("abort", () => controller.abort());
}
const timeoutId = setTimeout(() => {
controller.abort();
}, timeout);
try {
const response = await fetch(url, {
...fetchOptions,
signal: controller.signal,
});
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
if (error.name === "AbortError" && !fetchOptions.signal?.aborted) {
throw new Error(`Request timed out after ${timeout}ms`);
}
throw error;
}
}
// Usage
const response = await fetchWithTimeout("/api/slow-endpoint", { timeout: 5000 });Modern Alternative: AbortSignal.timeout()
const response = await fetch("/api/data", {
signal: AbortSignal.timeout(5000),
});FetchManager: Track and Cancel All Requests
class FetchManager {
constructor() {
this.controllers = new Map();
this.requestId = 0;
}
async fetch(url, options = {}) {
const id = ++this.requestId;
const controller = new AbortController();
this.controllers.set(id, { controller, url });
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
});
this.controllers.delete(id);
return response;
} catch (error) {
this.controllers.delete(id);
throw error;
}
}
cancel(id) {
const entry = this.controllers.get(id);
if (entry) {
entry.controller.abort();
this.controllers.delete(id);
}
}
cancelAll() {
for (const [id, { controller }] of this.controllers) {
controller.abort();
}
this.controllers.clear();
}
getPending() {
return Array.from(this.controllers.entries()).map(([id, { url }]) => ({
id,
url,
}));
}
}
const manager = new FetchManager();
// Track all fetches
const response = await manager.fetch("/api/data");
// View pending requests
console.log(manager.getPending());
// Cancel everything on page unload
window.addEventListener("beforeunload", () => manager.cancelAll());SPA Cleanup Patterns
Per-Route Controller
class Router {
constructor() {
this.currentController = null;
}
navigate(path) {
// Cancel all pending requests from the previous route
if (this.currentController) {
this.currentController.abort();
}
this.currentController = new AbortController();
const signal = this.currentController.signal;
// Pass signal to the route handler
this.loadRoute(path, signal);
}
async loadRoute(path, signal) {
try {
const data = await fetch(`/api${path}`, { signal }).then((r) => r.json());
this.render(data);
} catch (error) {
if (error.name === "AbortError") return;
this.renderError(error);
}
}
render(data) { /* ... */ }
renderError(error) { /* ... */ }
}Component Lifecycle
class DataWidget {
constructor(element) {
this.element = element;
this.controller = new AbortController();
}
async load() {
try {
const res = await fetch("/api/widget-data", {
signal: this.controller.signal,
});
const data = await res.json();
this.element.innerHTML = this.render(data);
} catch (error) {
if (error.name === "AbortError") return;
this.element.innerHTML = "<p>Failed to load</p>";
}
}
destroy() {
this.controller.abort();
this.element.innerHTML = "";
}
render(data) { return `<p>${data.value}</p>`; }
}Rune AI
Key Insights
- One abort cancels many: Pass the same signal to multiple
fetch()calls to cancel an entire group with a singleabort()call - Always check for AbortError: In every catch block, check
error.name === "AbortError"to silently ignore intentional cancellations - Clear timeouts in finally: Prevent accidental aborts after successful responses by clearing
setTimeoutin thefinallyblock - Cancel on route change in SPAs: Store the controller per route and abort in the cleanup/navigation handler to prevent stale renders
- Double-check query identity: After an async response, verify the query still matches the current query before rendering to prevent visual flicker
Frequently Asked Questions
Does canceling a Fetch save bandwidth?
Can I cancel a Fetch after the headers arrive but before the body?
Why do I get AbortError even after the response arrives?
How do I cancel Fetch in React useEffect?
Is there a performance cost to creating many AbortControllers?
Conclusion
Canceling Fetch requests prevents stale data rendering, saves bandwidth, and avoids state updates on destroyed components. The core pattern is: create an AbortController, pass signal to fetch(), call abort() on cleanup. For AbortController beyond Fetch (event listeners, computed workflows), see using AbortController in JS complete tutorial. For the retry pattern that works with cancellation, see API retry patterns in JavaScript full tutorial. For the event loop that schedules these async operations, see the JS event loop architecture complete guide.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.