PureDevTools

JavaScript AbortController Reference

Fetch cancellation, timeout patterns, AbortSignal.any(), event cleanup — with live code runner

All processing happens in your browser. No data is sent to any server.
MemberTypeDescription
new AbortController()constructorCreates a controller with a fresh, non-aborted signal
controller.signalAbortSignal (read-only)The associated AbortSignal — pass to cancellable APIs
controller.abort(reason?)methodAborts the signal; fires listeners; idempotent
signal.abortedboolean (read-only)false initially, true permanently after abort()
signal.reasonany (read-only)Abort reason; undefined before abort; DOMException AbortError if no reason given
new AbortController()Core API
const controller = new AbortController()

Creates a new AbortController instance. The controller acts as a handle for controlling cancellation — it exposes a signal property (an AbortSignal) that can be passed to cancellable APIs, and an abort() method to trigger cancellation. One controller manages exactly one signal.

controller.signalCore API
controller.signal

Read-only property. Returns the AbortSignal object associated with this controller. The signal can be passed to any API that accepts an AbortSignal for cancellation. When controller.abort() is called, the signal transitions to the aborted state and fires an 'abort' event to all listeners.

controller.abort()Core API
controller.abort(reason?)

Triggers cancellation on the associated AbortSignal. Sets signal.aborted to true, stores the provided value as signal.reason (or a DOMException AbortError if omitted), and synchronously fires all registered 'abort' event listeners. Subsequent calls after the first abort have no effect — the signal stays aborted with its original reason.

signal.abortedCore API
signal.aborted

Read-only boolean property. Returns false if the signal has not yet been aborted, and true once the associated controller's abort() method is called. Also true immediately for signals created via AbortSignal.abort(). The value never reverts from true to false.

signal.reasonCore API
signal.reason

Read-only property. Returns the abort reason that was passed to controller.abort(reason). Returns undefined if the signal has not been aborted. Returns DOMException{ name: 'AbortError' } if abort() was called without an explicit reason. The value is set atomically with the aborted flag and never changes.

Your search-as-you-type input fires a fetch request on every keystroke. The user types “react” — that’s 5 requests in 500ms, and the responses come back out of order: “reac” finishes after “react,” overwriting the correct results. You need to cancel the previous request when a new one starts. Or your component unmounts while a fetch is in flight, causing a “setState on unmounted component” warning. Both problems are solved by AbortController, but the API has grown beyond basic fetch cancellation — AbortSignal.timeout(), AbortSignal.any(), event listener cleanup, and custom async patterns.

Why This Reference (Not MDN)

MDN covers AbortController across multiple pages (AbortController, AbortSignal, fetch, addEventListener). This reference puts everything on one page — fetch cancellation, AbortSignal.timeout(), AbortSignal.any(), event listener cleanup with signal, and patterns for custom async cancellation — with interactive examples.

What Is AbortController?

AbortController is a Web platform API that lets you cancel asynchronous operations. It works with the Fetch API, addEventListener, and any custom code that accepts an AbortSignal. The controller holds a signal property — pass the signal to cancellable APIs, and call abort() when you want to cancel.

const controller = new AbortController();

// Pass the signal to any cancellable API
fetch("/api/data", { signal: controller.signal })
  .then(r => r.json())
  .catch(err => {
    if (err.name === "AbortError") return;  // cancelled — ignore
    throw err;  // real error — rethrow
  });

// Cancel the request:
controller.abort();

Availability: Chrome 66+, Firefox 57+, Safari 11.1+, Node.js 15+.


Core API

new AbortController()

const controller = new AbortController();
console.log(controller.signal.aborted);  // false
controller.abort("user navigated");
console.log(controller.signal.aborted);  // true
console.log(controller.signal.reason);   // "user navigated"

An AbortController is single-use — once aborted, the signal stays aborted. Create a new controller for each new cancellable operation.

controller.signal

The signal property returns the same AbortSignal object on every access. Pass it to multiple APIs — aborting the controller cancels all of them:

const controller = new AbortController();
const { signal } = controller;

// Share one signal across multiple APIs:
fetch("/api/users", { signal });
fetch("/api/posts", { signal });
document.addEventListener("scroll", onScroll, { signal });

// One abort() cancels all three:
controller.abort("navigated away");

controller.abort(reason?)

// Default reason: DOMException{ name: 'AbortError' }
controller.abort();
console.log(controller.signal.reason.name);  // AbortError

// Custom reason: any value
controller2.abort(new Error("superseded by newer request"));
controller3.abort("user clicked cancel");

abort() is idempotent — calling it a second time has no effect. The signal keeps the reason from the first call.


AbortSignal Properties

signal.aborted

Boolean read-only property. false initially, true permanently after abort().

const controller = new AbortController();
const { signal } = controller;

// Poll in a loop to respect cancellation:
for (let i = 0; i < 1000; i++) {
  if (signal.aborted) break;
  processItem(i);
}

signal.reason

The value passed to abort(). undefined before any abort. DOMException{ name: 'AbortError' } if aborted without a reason.

const controller = new AbortController();
console.log(controller.signal.reason);  // undefined

controller.abort(new Error("rate limited"));
console.log(controller.signal.reason.message);  // rate limited

Signal Factory Methods

AbortSignal.abort(reason?)

Returns an already-aborted signal. Useful for conditionally pre-cancelling operations:

function getSignal(disabled) {
  return disabled
    ? AbortSignal.abort("feature disabled")
    : new AbortController().signal;
}

const signal = getSignal(true);
console.log(signal.aborted);  // true

Availability: Chrome 88+, Firefox 88+, Safari 15.4+, Node.js 17.3+.

AbortSignal.timeout(milliseconds)

Returns a signal that automatically aborts after the given milliseconds with DOMException{ name: 'TimeoutError' }:

// Fetch with 5-second timeout — one line, no cleanup needed:
const response = await fetch("/api/data", {
  signal: AbortSignal.timeout(5000),
});

// In catch:
if (err.name === "TimeoutError") {
  console.log("Request timed out");
} else if (err.name === "AbortError") {
  console.log("Request was cancelled");
}

Note: AbortSignal.timeout() does not cancel on component unmount. Use AbortSignal.any([controller.signal, AbortSignal.timeout(ms)]) for React hooks.

Availability: Chrome 103+, Firefox 100+, Safari 16+, Node.js 17.3+.

AbortSignal.any(signals)

Returns a composite signal that aborts when any input signal aborts — perfect for combining a timeout with a user-cancel signal:

const userCancel = new AbortController();

const signal = AbortSignal.any([
  userCancel.signal,
  AbortSignal.timeout(5000),  // also abort on timeout
]);

fetch("/api/data", { signal });

// Cancel from any source:
userCancel.abort("stop button");  // OR the 5s timer fires first

Availability: Chrome 116+, Firefox 116+, Safari 17.4+, Node.js 20.3+.

signal.throwIfAborted()

Throws signal.reason if aborted; does nothing otherwise. The recommended way to check cancellation in custom async code:

async function processAll(items, { signal } = {}) {
  signal?.throwIfAborted();  // fail fast at entry

  for (const item of items) {
    signal?.throwIfAborted();  // check before each step
    await processItem(item, { signal });
  }
}

Availability: Chrome 100+, Firefox 97+, Safari 15.4+, Node.js 17.3+.


Common Patterns

Fetch Cancellation

async function fetchUser(id) {
  const controller = new AbortController();

  // Cancel if user navigates: save controller.abort to a variable
  // and call it from a navigation event or component cleanup.

  try {
    const res = await fetch(`/api/users/${id}`, {
      signal: controller.signal,
    });
    return await res.json();
  } catch (err) {
    if (err.name === "AbortError") return null;  // cancelled
    throw err;
  }
}

Always create a new controller per request — a used controller’s signal stays aborted.

Timeout with Fetch

// Modern: AbortSignal.timeout() — one liner
const res = await fetch("/api", { signal: AbortSignal.timeout(3000) });

// Manual: setTimeout + AbortController — works everywhere, allows cleanup
function fetchWithTimeout(url, ms) {
  const controller = new AbortController();
  const id = setTimeout(() => controller.abort(new Error("timeout")), ms);
  return fetch(url, { signal: controller.signal })
    .finally(() => clearTimeout(id));
}

Event Listener Cleanup

// Traditional: must manually track and remove each listener
const handler = (e) => handle(e);
document.addEventListener("click", handler);
window.addEventListener("resize", handler);
// cleanup: removeEventListener x2

// Signal-based: one abort() removes them all
const controller = new AbortController();
const { signal } = controller;

document.addEventListener("click", handler, { signal });
window.addEventListener("resize", handler, { signal });
form.addEventListener("submit", onSubmit, { signal });

// One call removes all three:
controller.abort();

Availability of the signal option: Chrome 90+, Firefox 88+, Safari 15+, Node.js 15+.

Custom Cancellable Async Functions

// Wrap a non-cancellable Promise (e.g., setTimeout):
function delay(ms, { signal } = {}) {
  return new Promise((resolve, reject) => {
    if (signal?.aborted) { reject(signal.reason); return; }
    const id = setTimeout(resolve, ms);
    signal?.addEventListener("abort", () => {
      clearTimeout(id);
      reject(signal.reason);
    }, { once: true });
  });
}

// Accept signal in your own async functions:
async function processPage(page, { signal } = {}) {
  signal?.throwIfAborted();
  const items = await fetchPage(page, { signal });
  signal?.throwIfAborted();
  return transformItems(items);
}

Combining Signals

// Modern: AbortSignal.any() (Chrome 116+, Node.js 20.3+)
const combined = AbortSignal.any([userSignal, AbortSignal.timeout(5000)]);

// Universal: manual merge with automatic cleanup
function mergeSignals(...signals) {
  const controller = new AbortController();
  for (const sig of signals) {
    if (sig.aborted) { controller.abort(sig.reason); break; }
    sig.addEventListener("abort", () => controller.abort(sig.reason),
      { signal: controller.signal });  // auto-cleanup when combined aborts
  }
  return controller.signal;
}

React useEffect Cleanup

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    const controller = new AbortController();

    fetch(`/api/users/${userId}`, { signal: controller.signal })
      .then(r => r.json())
      .then(setUser)
      .catch(err => {
        if (err.name !== "AbortError") throw err;
      });

    // Runs on unmount AND when userId changes:
    return () => controller.abort();
  }, [userId]);

  return user ? <div>{user.name}</div> : <p>Loading...</p>;
}

Without the cleanup function, a resolved fetch after unmount will try to setState on an unmounted component.


structuredClone() vs AbortController

FeatureAbortControllersetTimeout workaround
Cancel fetchNative supportNot supported
Auto event listener cleanup{ signal } optionManual removeEventListener
Abort reasonsignal.reasonN/A
Combine abort sourcesAbortSignal.any()Complex manual wiring
React cleanupIdiomatic useEffect returnTimeout-based hacks

Frequently Asked Questions

Can I reuse an AbortController after calling abort()?

No. Once aborted, the signal is permanently aborted. Create a new AbortController for each new operation.

Why does fetch throw AbortError instead of my custom reason?

The fetch implementation throws its own DOMException{ name: 'AbortError' }. Your custom reason is accessible via signal.reason, not the thrown error. Use signal.throwIfAborted() in your own code to re-throw the exact reason.

How do I cancel multiple fetch requests at once?

Share one signal across all requests:

const controller = new AbortController();
const { signal } = controller;

Promise.all([
  fetch("/api/a", { signal }),
  fetch("/api/b", { signal }),
  fetch("/api/c", { signal }),
]);

controller.abort();  // cancels all three

What is the difference between AbortError and TimeoutError?

AbortError comes from controller.abort() (manual cancellation). TimeoutError comes from AbortSignal.timeout() (automatic timeout). Both are DOMException values — check err.name to distinguish them.

Is AbortController available in Web Workers and Service Workers?

Yes. AbortController and AbortSignal are available in all worker contexts including Web Workers, Service Workers, and Shared Workers.

How do I implement a one-shot event listener that also cancels on abort?

target.addEventListener("click", handler, { once: true, signal });

The { once: true, signal } combination means the listener fires at most once and is also removed if the signal aborts — whichever comes first.

Related Tools

More JavaScript Tools