Skip to Content
JavaScript Advanced: Code That Holds Up in Production
Advanced
0 likes
Courses
JavaScript Advanced: Code That Holds Up in Production

Cancelling Work With AbortController

Cancel fetch calls, add timeouts with AbortSignal.timeout, and make your own async functions cancellable. After this, a slow request stops when the user leaves instead of finishing for nobody.

Course content

Sign in to save your progress.

Course Home

Skip ahead: If you only need a timeout on one fetch call, the one-liner is in "Timeouts in one line".

A request that nobody is waiting for still uses a connection, still costs the server work, and can still come back and overwrite newer data on the page. The user typed "lap", then "laptop", and the slow results for "lap" arrive last and win. AbortController is the standard way to say "stop, I don't need this anymore". fetch supports it, Node's file and timer functions support it, and your own functions can too.

The basic pattern

An AbortController has a signal and an abort() method. You pass the signal to the work, and call abort() when you want it to stop.

import http from "node:http";

const server = http.createServer((req, res) => {
  setTimeout(() => res.end("slow answer"), 2000);
}).listen(0);
const url = `http://localhost:${server.address().port}/`;

const controller = new AbortController();
setTimeout(() => controller.abort(), 100);

try {
  await fetch(url, { signal: controller.signal });
} catch (error) {
  console.log(error.name + ": " + error.message);
} finally {
  server.closeAllConnections();
  server.close();
}
AbortError: This operation was aborted

The examples in this lesson start a small local server that answers slowly, so you can see real cancellations without depending on someone else's API. Save them as .mjs.

When the signal aborts, fetch rejects with an AbortError, and the connection is closed. The server may still finish its work, but your code stops waiting and the response is thrown away.

Timeouts in one line

fetch has no timeout option, and by default it can wait a very long time. AbortSignal.timeout(ms) gives you a signal that aborts itself:

import http from "node:http";

const server = http.createServer((req, res) => {
  setTimeout(() => res.end("slow answer"), 2000);
}).listen(0);
const url = `http://localhost:${server.address().port}/`;

try {
  await fetch(url, { signal: AbortSignal.timeout(300) });
} catch (error) {
  console.log(error.name + ": " + error.message);
} finally {
  server.closeAllConnections();
  server.close();
}
TimeoutError: The operation was aborted due to timeout

Notice the name: TimeoutError, not AbortError. That lets you tell the two apart:

try {
  const response = await fetch(url, { signal });
} catch (error) {
  if (error.name === "TimeoutError") showMessage("The server took too long. Try again.");
  else if (error.name === "AbortError") return; // we cancelled it ourselves, nothing to show
  else throw error;
}

💡 Tip: A user cancel usually needs no error message at all. They know they cancelled. Showing "Request failed" for it looks like a bug.

Cancel or timeout, whichever comes first

AbortSignal.any() combines signals. The result aborts when any of them does:

const userCancel = new AbortController();
cancelButton.onclick = () => userCancel.abort();

const signal = AbortSignal.any([userCancel.signal, AbortSignal.timeout(10_000)]);
const response = await fetch("/api/report", { signal });

Only keep the latest search

The "lap" / "laptop" race from the start, fixed:

let current;

async function search(term) {
  current?.abort();
  current = new AbortController();
  try {
    const response = await fetch(`/api/search?q=${encodeURIComponent(term)}`, { signal: current.signal });
    showResults(await response.json());
  } catch (error) {
    if (error.name !== "AbortError") throw error;
  }
}

Every new search aborts the one before it, so an old response can never arrive and overwrite a newer one.

Make your own function cancellable

Anything that takes a while can accept a signal. Check it between steps, and pass it on to anything that already supports it:

async function importRows(rows, { signal } = {}) {
  let done = 0;
  for (const row of rows) {
    signal?.throwIfAborted();
    await new Promise(resolve => setTimeout(resolve, 10));
    done++;
  }
  return done;
}

const controller = new AbortController();
setTimeout(() => controller.abort(new Error("Import cancelled by user")), 35);

try {
  await importRows(Array.from({ length: 100 }), { signal: controller.signal });
} catch (error) {
  console.log(error.message);
}
Import cancelled by user

throwIfAborted() throws the abort reason. You can pass your own reason to abort(), as here, or leave it out and get a standard AbortError.

⚠ Warning: Aborting doesn't undo anything. If row 3 was already saved when you cancelled, it stays saved. Cancelling is for work that's still waiting, not a rollback. If you need all-or-nothing, use a database transaction.

Clean up

An AbortController is used once. After abort(), its signal stays aborted, so make a new one for the next request. And if you add an "abort" listener to a long-lived signal, remove it when the work finishes, or pass { once: true }.

Common mistakes

  • No timeout on server-side fetch calls. One slow upstream service and your requests pile up.
  • Showing an error message when the user cancelled on purpose.
  • Reusing an aborted controller for the next request. It aborts immediately.
  • Using Promise.race with a timer as a "timeout". It stops waiting but doesn't stop the request.
  • Treating abort as rollback.

What happens if you skip this

Your code works on a fast connection. On a slow one, old responses overwrite new ones, server-side calls hang until something crashes, and users click "Search" five times because nothing seems to happen.

In the search function, what would the user see if you removed the current?.abort() line and typed quickly?

About

Cancel fetch calls, add timeouts with AbortSignal.timeout, and make your own async functions cancellable. After this, a slow request stops when the user leaves instead of finishing for nobody.

Duration
5 min
Last update
08/21/2026
Was this helpful?
​ 0 ​ 0

Comments (0)

Sign in to comment

No comments yet.