Using AbortController in JS: Complete Tutorial
A complete tutorial on using AbortController in JavaScript. Covers canceling Fetch requests, implementing timeouts, aborting multiple requests with one signal, AbortSignal.timeout(), AbortSignal.any(), canceling event listeners, integrating with async iterators, and building cancelable async workflows.
AbortController is the standard mechanism for canceling asynchronous operations in JavaScript. It works with the Fetch API, event listeners, streams, and any custom async code. This guide covers every pattern from basic request cancellation to building fully cancelable async workflows.
How AbortController Works
// 1. Create a controller
const controller = new AbortController();
// 2. Pass its signal to an async operation
fetch("/api/data", { signal: controller.signal });
// 3. Call abort() to cancel
controller.abort();
// 4. The operation throws an AbortErrorThe AbortController creates an AbortSignal. When you call controller.abort(), the signal's aborted property becomes true and any operation watching that signal is canceled.
Basic Fetch Cancellation
async function fetchWithCancel(url) {
const controller = new AbortController();
// Cancel button
const cancelBtn = document.getElementById("cancel");
cancelBtn.addEventListener("click", () => controller.abort());
try {
const response = await fetch(url, { signal: controller.signal });
const data = await response.json();
return data;
} catch (error) {
if (error.name === "AbortError") {
console.log("Request was canceled by the user");
return null;
}
throw error; // Re-throw non-abort errors
}
}The key pattern: always check error.name === "AbortError" to distinguish cancellation from real errors.
Timeout With AbortController
async function fetchWithTimeout(url, options = {}, timeoutMs = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
});
return response;
} catch (error) {
if (error.name === "AbortError") {
throw new Error(`Request to ${url} timed out after ${timeoutMs}ms`);
}
throw error;
} finally {
clearTimeout(timeoutId);
}
}AbortSignal.timeout() (Modern Browsers)
// Built-in timeout signal (no manual setTimeout needed)
async function fetchWithBuiltinTimeout(url, timeoutMs = 5000) {
try {
const response = await fetch(url, {
signal: AbortSignal.timeout(timeoutMs),
});
return response.json();
} catch (error) {
if (error.name === "TimeoutError") {
console.error(`Request timed out after ${timeoutMs}ms`);
} else if (error.name === "AbortError") {
console.error("Request was aborted");
}
throw error;
}
}| Error Type | error.name | Cause |
|---|---|---|
| Manual abort | "AbortError" | controller.abort() called |
| Signal timeout | "TimeoutError" | AbortSignal.timeout() expired |
| Network failure | "TypeError" | Network unreachable, DNS failure |
Canceling Multiple Requests
One controller can cancel many operations:
async function loadDashboard() {
const controller = new AbortController();
const { signal } = controller;
// Cancel all on navigation
window.addEventListener("beforeunload", () => controller.abort());
try {
const [users, projects, stats] = await Promise.all([
fetch("/api/users", { signal }).then((r) => r.json()),
fetch("/api/projects", { signal }).then((r) => r.json()),
fetch("/api/stats", { signal }).then((r) => r.json()),
]);
return { users, projects, stats };
} catch (error) {
if (error.name === "AbortError") {
console.log("Dashboard load canceled");
return null;
}
throw error;
}
}AbortSignal.any() (Combining Signals)
// Cancel if EITHER user clicks cancel OR timeout expires
async function fetchWithCancelAndTimeout(url) {
const userController = new AbortController();
document.getElementById("cancel").addEventListener("click", () => {
userController.abort();
});
const combinedSignal = AbortSignal.any([
userController.signal,
AbortSignal.timeout(10000),
]);
const response = await fetch(url, { signal: combinedSignal });
return response.json();
}AbortSignal.any() creates a signal that aborts when any of the input signals abort. This is the signal equivalent of Promise.race.
Canceling Event Listeners
function setupTemporaryListener(element, event, handler, timeoutMs = 5000) {
const controller = new AbortController();
element.addEventListener(event, handler, { signal: controller.signal });
// Auto-remove after timeout
setTimeout(() => controller.abort(), timeoutMs);
return controller;
}
// Usage: listen for a click for 5 seconds, then stop
const ctrl = setupTemporaryListener(document.body, "click", (e) => {
console.log("Clicked:", e.target);
});
// Or cancel early
ctrl.abort();Removing Multiple Listeners at Once
function setupMouseTracking(element) {
const controller = new AbortController();
const { signal } = controller;
element.addEventListener("mouseenter", onEnter, { signal });
element.addEventListener("mousemove", onMove, { signal });
element.addEventListener("mouseleave", onLeave, { signal });
window.addEventListener("resize", onResize, { signal });
// One call removes ALL four listeners
return () => controller.abort();
}
const cleanup = setupMouseTracking(document.getElementById("canvas"));
// Later: cleanup();Debounced Search With Cancellation
class DebouncedSearch {
constructor(apiUrl, delay = 300) {
this.apiUrl = apiUrl;
this.delay = delay;
this.controller = null;
this.timeoutId = null;
}
search(query) {
// Cancel previous debounce timer
clearTimeout(this.timeoutId);
// Cancel previous in-flight request
if (this.controller) {
this.controller.abort();
}
return new Promise((resolve, reject) => {
this.timeoutId = setTimeout(async () => {
this.controller = new AbortController();
try {
const res = await fetch(
`${this.apiUrl}?q=${encodeURIComponent(query)}`,
{ signal: this.controller.signal }
);
const data = await res.json();
resolve(data);
} catch (error) {
if (error.name !== "AbortError") reject(error);
// Silently ignore abort errors
}
}, this.delay);
});
}
cancel() {
clearTimeout(this.timeoutId);
if (this.controller) this.controller.abort();
}
}
const search = new DebouncedSearch("/api/search", 300);See building a search bar with JS debouncing guide for a complete search component.
Cancelable Async Workflow
async function processWorkflow(steps, signal) {
const results = [];
for (const step of steps) {
// Check before each step
if (signal?.aborted) {
throw new DOMException("Workflow canceled", "AbortError");
}
const result = await step(signal);
results.push(result);
}
return results;
}
// Usage
const controller = new AbortController();
const workflow = processWorkflow(
[
(signal) => fetch("/api/step1", { signal }).then((r) => r.json()),
(signal) => fetch("/api/step2", { signal }).then((r) => r.json()),
async (signal) => {
// Long computation with periodic abort check
for (let i = 0; i < 1000000; i++) {
if (i % 10000 === 0 && signal?.aborted) {
throw new DOMException("Canceled", "AbortError");
}
// ... compute
}
},
(signal) => fetch("/api/step3", { signal }).then((r) => r.json()),
],
controller.signal
);
// Cancel from UI
document.getElementById("stop").addEventListener("click", () => controller.abort());AbortController API Reference
| Property/Method | Description |
|---|---|
controller.signal | The AbortSignal associated with this controller |
controller.abort(reason) | Aborts with optional reason |
signal.aborted | Boolean, true if aborted |
signal.reason | The abort reason (default: DOMException) |
signal.throwIfAborted() | Throws if already aborted |
signal.addEventListener("abort", fn) | Listen for abort event |
AbortSignal.timeout(ms) | Static: creates signal that aborts after ms |
AbortSignal.any(signals) | Static: creates signal that aborts when any input aborts |
Rune AI
Key Insights
- One controller, many operations: Pass the same signal to multiple Fetch calls, event listeners, or async steps to cancel them all at once
- Always check error.name for "AbortError": Distinguish intentional cancellation from real errors in catch blocks
- AbortSignal.timeout() replaces manual setTimeout: Cleaner syntax and throws
TimeoutErrorinstead ofAbortError - AbortSignal.any() composes signals: Combine user cancellation, timeout, and route-change signals into one
- Controllers are single-use: Once aborted, a signal is permanently aborted; create a new controller for the next operation
Frequently Asked Questions
Does aborting a Fetch actually stop the server from processing?
Can I reuse an AbortController after calling abort?
What is the difference between AbortError and TimeoutError?
Should I use AbortController or the older CancelToken (Axios)?
How do I cancel a group of requests in a SPA route change?
Conclusion
AbortController is the universal cancellation primitive in JavaScript. It cancels Fetch requests, removes event listeners, composes with AbortSignal.any() for combined timeout and user cancellation, and integrates into multi-step async workflows. The core pattern is: create a controller, pass its signal, and call abort() when needed. For Fetch fundamentals, see how to use the JS Fetch API complete tutorial. For cancelable request cancellation patterns, see canceling Fetch requests in JavaScript full 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.