AbortController

Reviewed & published by Brayan K

AbortController is a built-in object that produces a reusable signal you hand to async work, so calling its abort() method can cancel a fetch, timer, or any operation listening to that signal.

Part of the free JavaScript course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

It is how you stop work you no longer need: cancelling a stale search request, tearing down a fetch when a component unmounts, or enforcing a hard timeout.

What You'll Learn in This Lesson

📚 Prerequisites: You should know promises and async/await with try/catch, since aborting surfaces as a rejected promise.

🔔 Real-World Analogy: An AbortController is a fire alarm. The controller is the pull handle; the signal is the wiring that runs to every room. You give the signal to each async task ("listen for the alarm"). Pulling the handle (abort()) rings everywhere at once and every task that was listening stops what it is doing.

1️⃣ The Controller and Its Signal

Create a controller with new AbortController(). It exposes a read-only signal that you pass to the operation you want to be able to cancel, and an abort() method that flips that signal to the aborted state. The signal has an aborted boolean and fires an "abort" event when triggered.

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

// React the moment the signal is tripped
signal.addEventListener("abort", () => {
  console.log("abort event! reason:", signal.reason);
});

console.log("before:", signal.aborted);
// ✅ Expected output: before: false

controller.abort("user navigated away");

console.log("after:", signal.aborted);
// ✅ Expected output: abort event! reason: user navigated away
// ✅ Expected output: after: true

2️⃣ Cancelling Async Work & Catching AbortError

Pass signal to fetch and call abort() to cancel the request. The fetch promise rejects with an AbortError, which you catch and distinguish from a real network failure by checking err.name. This is the classic "cancel the previous search as the user types" pattern:

⚠️ Always check err.name: Treating an AbortError as a real error shows users scary "request failed" messages for something they (or your code) caused on purpose.

The runnable demo below uses a cancellable delay that listens to the signal — exactly the shape fetch follows — so you can watch an abort reject with an AbortError in Node:

// A cancellable wait that rejects with an AbortError when the signal trips
function delay(ms, signal) {
  return new Promise((resolve, reject) => {
    if (signal.aborted) {
      return reject(new DOMException("Aborted", "AbortError"));
    }
    const id = setTimeout(resolve, ms);
    signal.addEventListener("abort", () => {
      clearTimeout(id);
      reject(new DOMException("Aborted", "AbortError"));
    });
  });
}

async function main() {
  const controller = new AbortController();

  // Cancel the 1000ms wait after only 20ms
  setTimeout(() => controller.abort(), 20);

  try {
    await delay(1000, controller.signal);
    console.log("finished waiting"); // never runs
  } catch (err) {
    if (err.name === "AbortError") {
      console.log("cancelled:", err.name);
      // ✅ Expected output: cancelled: AbortError
    } else {
      throw err;
    }
  }
}

main();

🎯 Your Turn #1 — Trip the Signal

Replace the blank with the method that cancels the controller so the event fires.

const controller = new AbortController();
controller.signal.addEventListener("abort", () => {
  console.log("stopped!"); // ✅ Expected output: stopped!
});

// 👉 replace ___ with the method that aborts the controller
controller.___();

console.log(controller.signal.aborted); // ✅ Expected output: true

3️⃣ AbortSignal.timeout — Deadlines for Free

When you only need a deadline, skip the controller entirely. AbortSignal.timeout(ms) returns a signal that auto-aborts after ms milliseconds. There is also AbortSignal.any([...]) to abort as soon as any of several signals fires — handy for "cancel on either a timeout or the user clicking stop".

function delay(ms, signal) {
  return new Promise((resolve, reject) => {
    const id = setTimeout(resolve, ms);
    signal.addEventListener("abort", () => {
      clearTimeout(id);
      reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
    });
  });
}

async function main() {
  // This signal cancels itself after 20ms
  const signal = AbortSignal.timeout(20);

  try {
    await delay(1000, signal); // wants 1s, but the signal fires first
    console.log("done");
  } catch (err) {
    console.log("timed out:", err.name);
    // ✅ Expected output: timed out: TimeoutError
  }
}

main();

🎯 Your Turn #2 — Identify the Cancellation

Replace the blank so the code checks for a deliberate abort instead of treating it as a real error.

function makeAbortError() {
  return new DOMException("Aborted", "AbortError");
}

try {
  throw makeAbortError();
} catch (err) {
  // 👉 replace ___ with the property that holds "AbortError"
  if (err.___ === "AbortError") {
    console.log("cancelled on purpose"); // ✅ Expected output: cancelled on purpose
  } else {
    console.log("real error");
  }
}

🧠 Quick Recall

Predict the output before revealing the answer.

true — abort() permanently flips the signal's aborted flag.

A DOMException with name === "AbortError".

No — the signal stays aborted. Create a fresh new AbortController() for each new operation.

Common Errors & Fixes

🎯 Mini Challenge — Cancel the Loser

Start two waits. When the fast one finishes, abort the slow one so its cleanup runs. This mirrors cancelling a stale request once fresher data arrives.

function delay(ms, signal, label) {
  return new Promise((resolve, reject) => {
    const id = setTimeout(() => resolve(label), ms);
    signal.addEventListener("abort", () => {
      clearTimeout(id);
      console.log(label, "was cancelled");
      reject(new DOMException("Aborted", "AbortError"));
    });
  });
}

async function main() {
  const slow = new AbortController();

  // Kick off the slow task (we may cancel it)
  const slowPromise = delay(500, slow.signal, "slow").catch(() => {});

  // The fast task wins
  const winner = await delay(20, new AbortController().signal, "fast");
  console.log("winner:", winner);
  // ✅ Expected output: winner: fast

  // No longer need the slow one -> cancel it
  slow.abort();
  await slowPromise;
  // ✅ Expected output: slow was cancelled
}

main();

Quick Reference — AbortController

GoalCode
Createconst c = new AbortController();
Pass to fetchfetch(url, { signal: c.signal })
Cancelc.abort();
Detecte.name === "AbortError"
Auto deadlineAbortSignal.timeout(3000)

📚 Keep Learning

🎉 Lesson Complete — AbortController!

Practice quiz

What two things does a new AbortController() give you?

  • A signal and an abort() method
  • A promise and a resolve() method
  • Two separate signals
  • A signal and a timeout

Answer: A signal and an abort() method. A controller exposes a read-only signal you hand to async work, plus an abort() method that trips that signal.

After controller.abort(), what is controller.signal.aborted?

  • false
  • true
  • undefined
  • It throws

Answer: true. abort() permanently flips the signal's aborted flag to true.

When you abort a fetch, the fetch promise rejects with what?

  • A plain Error
  • A DOMException whose name is 'AbortError'
  • null
  • A TypeError

Answer: A DOMException whose name is 'AbortError'. An aborted fetch rejects with a DOMException whose name is 'AbortError'.

How do you tell a deliberate cancellation apart from a real network failure?

  • Check err.message
  • Check err.code
  • Check err.name === 'AbortError'
  • You cannot tell them apart

Answer: Check err.name === 'AbortError'. Checking err.name === 'AbortError' distinguishes an intentional abort from a genuine error.

Can you reuse an AbortController after calling abort()?

  • Yes, it resets automatically
  • No — the signal stays aborted forever
  • Only if you call reset()
  • Only for fetch, not timers

Answer: No — the signal stays aborted forever. A signal is one-shot; once aborted it stays aborted, so you create a fresh controller per operation.

What does AbortSignal.timeout(ms) return?

  • A controller
  • A promise
  • A signal that auto-aborts after ms milliseconds
  • A number

Answer: A signal that auto-aborts after ms milliseconds. AbortSignal.timeout(ms) returns a signal that aborts itself after the given delay, with no controller needed.

Which value do you pass to fetch's options?

  • signal: controller
  • signal: controller.signal
  • controller: signal
  • abort: controller

Answer: signal: controller.signal. You pass the signal, not the controller: fetch(url, { signal: controller.signal }).

When a signal created by AbortSignal.timeout fires, signal.reason is a DOMException named what?

  • AbortError
  • TimeoutError
  • RangeError
  • DeadlineError

Answer: TimeoutError. A timeout signal aborts with a DOMException whose name is 'TimeoutError'.

What event does a signal fire when it is aborted?

  • 'cancel'
  • 'stop'
  • 'abort'
  • 'error'

Answer: 'abort'. The signal fires an 'abort' event, which any listener attached to that signal reacts to.

What combines several signals so an operation aborts when ANY of them fires?

  • AbortSignal.all([...])
  • AbortSignal.any([...])
  • AbortSignal.race([...])
  • AbortSignal.merge([...])

Answer: AbortSignal.any([...]). AbortSignal.any([...]) aborts as soon as any of the supplied signals fires.

Continue this course

Frequently asked questions

What is the relationship between AbortController and AbortSignal?

An AbortController is the remote control; its signal property is the receiver. You hand the signal to an async operation (like fetch) and keep the controller. Calling controller.abort() flips the signal's aborted flag to true and fires an 'abort' event, which any operation watching that signal reacts to. One controller can drive many operations that all share its single signal.

What happens to a fetch when its signal is aborted?

The fetch promise rejects with a DOMException whose name is 'AbortError'. You catch it like any rejection and can check err.name === 'AbortError' to tell a deliberate cancellation apart from a real network error. The browser also tears down the underlying request so the bytes stop coming over the wire.

Can I reuse an AbortController after calling abort()?

No — a signal is one-shot. Once aborted, it stays aborted forever, and any new operation you attach to that same signal aborts immediately. For each new cancellable operation (for example each new search keystroke), create a fresh AbortController so you get a clean, un-aborted signal.

What is AbortSignal.timeout?

AbortSignal.timeout(ms) is a shortcut that returns a signal which auto-aborts after the given number of milliseconds — no controller needed. It is perfect for putting a hard deadline on a fetch: pass it as the signal and the request cancels itself if it runs long. There is also AbortSignal.any([...]) to combine several signals into one.

Related lessons