cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Cancellation with AbortController

Stop fetch requests, timers, event listeners, and your own asynchronous work with AbortController and AbortSignal.

By the end you can
  • 01
    Separate controller from signalKeep the power to abort with the caller and pass the read-only signal to work.
  • 02
    Cancel browser work safelyAbort fetches, body reads, timeouts, and event listeners without leaking timers.
  • 03
    Design cancellable APIsAccept { signal }, check up front, listen once, clean up, and reject with the original reason.

Why cancellation exists

Asynchronous JavaScript lets work continue while your page stays responsive. That is wonderful until the work is no longer useful: the user typed a newer search, moved to another page, pressed Stop, or a request took too long. Cancellation is the pattern for telling that old work, “please stop now, clean up what you started, and reject the promise so the caller can move on.”

You have already met promises, timers, and error handling strategies. Cancellation connects all three: an operation receives a signal, checks it, listens for it, and rejects with a reason when the signal aborts.

Real-life analogyCancelling a restaurant order

Imagine ordering fries, then realizing you are at the wrong table. You can ask the waiter to cancel. If the kitchen has not started, nothing happens. If the fries are already cooked, cancelling your wait does not uncook them. That is how browser requests work too: you can stop waiting, but you cannot assume the server forgot.

In real life: You tell the waiter to cancel
In JavaScript: Code calls controller.abort()
In real life: The kitchen stops if it can
In JavaScript: The task clears timers, listeners, or a request
In real life: Food already cooked is still cooked
In JavaScript: Server-side effects are not automatically undone

Where the analogy stops: A restaurant may choose to refund you. JavaScript cancellation only delivers a signal; every API decides what cleanup it can safely do.

The one-line definition

AbortController creates an AbortSignal; passing that signal into asynchronous work lets the owner abort work that is no longer needed.

AbortController & AbortSignal

INTERACTIVE

The API is split into two objects because ownership matters. The AbortController has the power: abort(reason?). The AbortSignal is what work receives: it exposes aborted, reason, an abort event, and throwIfAborted(). Hand out the signal; keep the controller.

Real-life analogyA referee's whistle

A good referee does not hand the whistle to every player. The players only need to listen. In code, the same rule prevents helper functions from accidentally cancelling their caller’s other work.

In real life: The referee owns the whistle
In JavaScript: The caller owns new AbortController()
In real life: Players listen for the whistle
In JavaScript: Tasks receive controller.signal
In real life: One whistle blast stops play
In JavaScript: A signal aborts once and stays aborted

Where the analogy stops: A human referee can restart play with the same whistle. An AbortSignal cannot reset; create a new controller for the next operation.

Sort these responsibilities before you run the bigger demo.

Controller or signal?
  • Has the .abort(reason?) method
  • Passed to fetch(url, { signal })
  • Exposes .aborted and .reason
  • TimeoutError from AbortSignal.timeout(1000)
  • Cannot be reset after aborting
  • Usually kept private by the caller
Try it yourself
0 of 6 correct

Decide who owns each cancellation responsibility.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Now try a fake chunked download. It uses timers instead of a real network so you can see each chunk, but it follows the same pattern as a cancellable request: check first, pass the signal down, clear pending work when the signal aborts, and reject with the signal’s reason.

Cancellable download
Cancellable downloadPop out in the code editor (opens in a new tab)JavaScript
async function downloadFile({ signal }) {  signal.throwIfAborted();  for (const chunk of [1, 2, 3, 4]) {    await wait(350, { signal });    progress.value = chunk * 25;  }  return "download complete";}
Runtimeidle
0%
progress0%
signal.abortedfalse
signal.reason.name—
active timers0
Log
  1. Press Start to create a fresh controller.
Try it yourself

A fresh operation gets a fresh controller. Start normally, give it a deadline, or combine a manual abort with a timeout.

A fake chunked task driven by timers. Reset aborts the current run and clears every timer. The timeout and any helpers are supported by modern browsers and Node 20.3+.

Notice the reason names. controller.abort() without a custom reason creates a DOMException named AbortError in modern platforms. AbortSignal.timeout(ms) aborts with a DOMException named TimeoutError. A signal aborts only once, and the abort event fires synchronously during abort().

Cancelling fetch

BROWSER CHECK

fetch is the most common place you will use a signal. You pass it in the options object: fetch(url, { signal }). If the signal aborts before the request completes, modern fetch rejects with signal.reason. Older browsers often reported AbortError for every abort, so production code should care about the broad category more than exact messages.

Real fetch cancellation in your browser
Already-aborted fetchPop out in the code editor (opens in a new tab)JavaScript
const signal = AbortSignal.abort(); try {  await fetch(location.href, { signal });} catch (error) {  console.log(error.name);}
Browser resultsame origin
catchClick Run to fetch this page with an aborted signal.
Try it yourself

The signal is already aborted, so fetch rejects before doing useful work. This demo uses location.href, not an external URL.

Modern fetch rejects with the signal’s reason. Older browsers may always report AbortError.

Aborting after response headers arrive can still matter: if your code is reading a large body stream, aborting can make the body read fail. What abort does not do is roll back a server. If you sent a request that creates an order, the server may already have created it.

Treat aborts differently from failures

A user pressing Stop is usually not an error worth alarming them about. In a catch, check error.name or the signal state so you can ignore expected cancellation and still show real network failures.

AbortSignal.timeout & AbortSignal.any

ALARMS

You do not always need to create a controller yourself. AbortSignal.abort(reason?) returns a signal that is already aborted. AbortSignal.timeout(ms) returns a signal that aborts itself after a deadline. AbortSignal.any([...]) creates a combined signal that follows the first input signal to abort. The any helper is Baseline 2024 in browsers and available in modern Node.

Real-life analogyEgg timers and alarm clocks

A deadline signal is an egg timer that blows the whistle for you. A combined signal is like placing several alarms around the room: as soon as one rings, you wake up and the reason says which one rang.

In real life: An egg timer rings after one minute
In JavaScript: AbortSignal.timeout(1000) aborts itself
In real life: Any alarm can wake you
In JavaScript: AbortSignal.any([user, deadline]) follows the first abort
In real life: The first ringing alarm explains why you woke
In JavaScript: The combined signal keeps the first reason

Where the analogy stops: Real alarms can be snoozed. AbortSignals are one-shot; build a new signal for another deadline.

Three signal helpersPop out in the code editor (opens in a new tab)JavaScript
const alreadyDone = AbortSignal.abort(new DOMException("Nope", "AbortError")); const deadline = AbortSignal.timeout(1000); const controller = new AbortController();const cancelOrDeadline = AbortSignal.any([controller.signal, deadline]);

Use timeout when slow work should fail automatically. Use any when several events can stop the same operation: user navigated away, component unmounted, or a deadline expired.

Making your APIs cancellable

STEP THROUGH

Built-in APIs are useful, but your own helpers should cooperate too. The convention is to accept an options object with signal. That keeps the function extensible: later you can add retries, timeout, or other options without changing the call shape.

Cancellable sleep patternPop out in the code editor (opens in a new tab)JavaScript
export function sleep(ms, { signal } = {}) {  signal?.throwIfAborted();  return new Promise((resolve, reject) => {    const onAbort = () => {      clearTimeout(timer);      reject(signal.reason);    };    const timer = setTimeout(() => {      signal?.removeEventListener("abort", onAbort);      resolve("slept");    }, ms);    signal?.addEventListener("abort", onAbort, { once: true });  });} const controller = new AbortController();const promise = sleep(800, { signal: controller.signal });controller.abort(new DOMException("Stopped", "AbortError"));
Make your API cancellable
Step 0 of 10Ready
Your turn: follow the blue line

Choose a setting, predict whether sleep starts a timer, then step through the real cancellation pattern.

Running in
  1. script
Next: line 17
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
export function sleep(ms, { signal } = {}) {  signal?.throwIfAborted();  return new Promise((resolve, reject) => {    const onAbort = () => {      clearTimeout(timer);      reject(signal.reason);    };    const timer = setTimeout(() => {      signal?.removeEventListener("abort", onAbort);      resolve("slept");    }, ms);    signal?.addEventListener("abort", onAbort, { once: true });  });} const controller = new AbortController();controller.abort(new DOMException("Stopped", "AbortError"));
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Pick when the caller aborts

Changing the setting starts a fresh replay.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

The pattern is small but strict:

  1. Check up front with signal?.throwIfAborted().
  2. Start the real work only after that check.
  3. Add an abort listener with { once: true }.
  4. On abort, clean up and reject with signal.reason.
  5. On success, remove the listener and resolve normally.

Search-as-you-type race condition

INTERACTIVE

Search boxes often fire a request for every few characters. If the request for c is slow and the request for cat is fast, the old c result can finish last and overwrite the fresh result. That is not a promise bug; it is a race condition.

Search-as-you-type race
Abort stale searchesPop out in the code editor (opens in a new tab)JavaScript
let currentController; async function runSearch(query) {  currentController?.abort(new DOMException("Newer search", "AbortError"));  currentController = new AbortController();  const { signal } = currentController;  const results = await fakeSearch(query, { signal });  render(results);}
Rendered resultracey
screenNo search yet
Request log
  1. Run the preset typing sequence: c → ca → cat.
Try it yourself

No abort means the slow first query can finish last and overwrite the fresh result. That is the classic search-as-you-type race.

Latencies are fixed so the stale-result bug is reproducible. The fake search uses real timers and real AbortSignals.

The fix is to keep the previous controller in a variable. Before starting a newer request, abort the previous controller and create a new one. Only the latest request gets to render.

Abort can remove event listeners too

REAL BUTTON

addEventListener accepts a signal option. When the signal aborts, the browser removes that listener. This is a tidy way to clean up temporary UI, drag handles, keyboard shortcuts, or pop-up behavior that attaches several listeners at once.

One abort removes several listeners
Listener cleanupPop out in the code editor (opens in a new tab)JavaScript
const controller = new AbortController();const { signal } = controller; button.addEventListener("click", countClick, { signal });button.addEventListener("pointerenter", showHint, { signal });window.addEventListener("keydown", onKey, { signal }); controller.abort(); // removes all three listeners
Real buttonlisteners active
click listener count0
C key listener count0
Try it yourself

The demo button and the C key have listeners registered with one shared signal.

This uses real addEventListener calls with the { signal } option.

Where you’ll use this

Cancellation shows up anywhere the user can outpace old work:

  • Autocomplete and search-as-you-type requests.
  • Route changes: stop loading data for pages the user left.
  • Upload, download, import, and export Stop buttons.
  • Deadlines for slow operations.
  • Temporary event listeners that should disappear together.
A practical shapePop out in the code editor (opens in a new tab)JavaScript
let currentController; export async function loadProfile(userId) {  currentController?.abort(new DOMException("Newer profile", "AbortError"));  currentController = new AbortController();   try {    const response = await fetch("/api/users/" + userId, {      signal: currentController.signal,    });    return await response.json();  } catch (error) {    if (currentController.signal.aborted) return null;    throw error;  }}

Misconceptions and edge cases

HONESTY CHECK
Similar cancellation ideas that behave differently
IdeaWhat is trueCommon trap
Abort vs undoAbort stops waiting and local work.It does not roll back server-side effects.
AbortError vs TimeoutErrorThe reason tells you why the signal aborted.Do not assume every abort has the same name.
Controller vs signalThe controller aborts; the signal notifies.Passing controllers everywhere gives too much power.
Promise rejection vs cleanupRejecting reports the stop to callers.You still need to clear timers and listeners.
  • “Promises can be cancelled.” Promises report completion. The work behind them must cooperate with a signal.
  • “Abort undoes everything.” It does not undo effects already completed, especially on a server.
  • “I can reuse one controller.” Create a new controller per operation because a signal aborts only once.
  • “Timeouts are just AbortError.” Timeout signals use TimeoutError in modern platforms.
  • “If fetch responded, abort no longer matters.” Reading the body stream can still error after abort.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upPredict the reason

Read the code and type the exact error name printed.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const signal = AbortSignal.abort(new DOMException("Too slow", "TimeoutError"));
try {
  signal.throwIfAborted();
} catch (error) {
  console.log(error.name);
}

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeWrite a cancellable delay

    Complete the helper so callers can pass { signal }.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    function delay(ms, { signal } = {}) {
      // Your code here
    }
      Exercise 3 · PracticeFind the stale render bug

      What text is left on screen after these two renders?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      let rendered = "";
      function render(value) { rendered = value; }
      render("Results for cat");
      render("Results for c");
      console.log(rendered);

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeOne shot means one reason

        Predict the printed reason name.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const controller = new AbortController();
        controller.abort();
        controller.abort(new DOMException("Later", "TimeoutError"));
        console.log(controller.signal.reason.name);

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeDesign a cancellable search helper

          Sketch the shape of a helper that only lets the latest search win.

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          let currentController;
          
          async function searchLatest(query) {
            // abort the old one, create a new one, pass the signal to fetch
          }

            Check your understanding

            7 QUESTIONS
            Cancellation quiz · 7 questionsScore: first tries count
            1. Question 1 of 7Which object should you pass to a function that should be cancellable but should not control cancellation?

              Choose an answer to see the explanation.

            2. Question 2 of 7What does the aborted boolean snippet print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const controller = new AbortController();
              controller.abort();
              console.log(controller.signal.aborted);

              Choose an answer to see the explanation.

            3. Question 3 of 7What is the default reason when controller.abort() gets no reason?

              Choose an answer to see the explanation.

            4. Question 4 of 7What does the custom reason snippet print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const signal = AbortSignal.abort(new DOMException("Too slow", "TimeoutError"));
              try {
                signal.throwIfAborted();
              } catch (error) {
                console.log(error.name);
              }

              Choose an answer to see the explanation.

            5. Question 5 of 7What happens when AbortSignal.any([a, b]) is used?

              Choose an answer to see the explanation.

            6. Question 6 of 7What does the one-shot abort snippet print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const controller = new AbortController();
              controller.abort();
              controller.abort(new DOMException("Late", "TimeoutError"));
              console.log(controller.signal.reason.name);

              Choose an answer to see the explanation.

            7. Question 7 of 7Why should a cancellable API remove listeners or clear timers?

              Choose an answer to see the explanation.

            Key takeaways

            • Keep the AbortController; pass the AbortSignal.
            • A signal aborts once and keeps its first reason.
            • Modern fetch rejects with the signal’s reason; handle aborts separately from real failures.
            • AbortSignal.timeout creates deadlines; AbortSignal.any combines ways to stop.
            • Cancellable APIs check up front, listen once, clean up, reject with signal.reason, and pass signals down.

            Cancellation is cooperative: an owner aborts a signal, and well-designed work notices, cleans up, and rejects with the reason.

            Up next: Async patterns.

            CompleteFrontend Clear concepts. Working examples.