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.

JavaScriptintermediate
15 min read

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

javascriptjavascript
// 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 AbortError

The 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

javascriptjavascript
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

javascriptjavascript
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)

javascriptjavascript
// 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 Typeerror.nameCause
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:

javascriptjavascript
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)

javascriptjavascript
// 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

javascriptjavascript
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

javascriptjavascript
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

javascriptjavascript
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

javascriptjavascript
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/MethodDescription
controller.signalThe AbortSignal associated with this controller
controller.abort(reason)Aborts with optional reason
signal.abortedBoolean, true if aborted
signal.reasonThe 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

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 TimeoutError instead of AbortError
  • 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
RunePowered by Rune AI

Frequently Asked Questions

Does aborting a Fetch actually stop the server from processing?

No. `AbortController` cancels the client-side request and closes the connection, but the server may continue processing. The server receives a connection reset, which well-written servers handle gracefully.

Can I reuse an AbortController after calling abort?

No. Once aborted, the signal stays aborted permanently. Create a new `AbortController` for each new operation or request cycle.

What is the difference between AbortError and TimeoutError?

`AbortError` comes from manually calling `controller.abort()`. `TimeoutError` comes from `AbortSignal.timeout()`. Both cancel the operation, but the different names let you distinguish the cause.

Should I use AbortController or the older CancelToken (Axios)?

Use `AbortController`. Axios CancelToken is deprecated in favor of `AbortController` since Axios 0.22+. `AbortController` is the web standard. See [how to use Axios in JavaScript complete guide](/tutorials/programming-languages/javascript/how-to-use-axios-in-javascript-complete-guide) for Axios cancellation.

How do I cancel a group of requests in a SPA route change?

Create one `AbortController` per route/page. Pass its signal to all Fetch calls. In the route cleanup function, call `controller.abort()`. This cancels every pending request for that route.

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.