Programatium Workshop
Web APIs

Cancel Fetch Requests with AbortController

Cancel Fetch Requests with AbortController
In briefCancel a fetch request by creating an `AbortController` and passing its `signal` to `fetch()`, then calling `controller.abort()` when the user cancels, a newer request supersedes it, a timeout expires, or the owning view is removed. Create a new controller for each operation because an aborted signal stays aborted. In `catch`, identify cancellation from the signal you own rather than treating every network error as an abort. Clear timeout handles and interface state in `finally`. Client cancellation does not guarantee that a server stopped work or reversed a completed side effect.

Pass an abort signal into fetch

In browser JavaScript, fetch() does not return a cancel method. Cancellation travels through an AbortSignal, usually created by an AbortController.

const controller = new AbortController();

const responsePromise = fetch("/api/search?q=otter", {
  signal: controller.signal,
});

controller.abort();

Calling abort() marks the signal as aborted and causes the fetch operation to reject if it has not already completed. According to MDN's AbortController reference, cancellation can also interrupt consumption of a response body or stream.

One controller may signal several operations, but it is single-use: once aborted, its signal stays aborted. Create a fresh controller for a new request. Reusing the old one produces an immediate rejection, which is correct behavior and a surprisingly efficient way to make a search box appear haunted.

Build a cancelable JSON helper

Let the caller own cancellation by accepting a signal:

async function fetchJson(url, { signal } = {}) {
  const response = await fetch(url, { signal });

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }

  return response.json();
}

fetch() resolves after an HTTP response even when its status is 404 or 500, so check response.ok or the status explicitly. Network failure, cancellation, HTTP failure, and invalid JSON are different outcomes. Do not turn all of them into “No results.”

The Fetch Standard defines the request's associated abort signal and rejection behavior. Application code still decides how to present that outcome.

Connect cancellation to interface state

This example cancels an earlier search before starting the next one:

let activeController = null;

async function runSearch(query) {
  activeController?.abort("Superseded by a newer search");
  activeController = new AbortController();
  const controller = activeController;

  try {
    const data = await fetchJson(
      `/api/search?q=${encodeURIComponent(query)}`,
      { signal: controller.signal },
    );

    if (activeController === controller) {
      renderResults(data);
    }
  } catch (error) {
    if (controller.signal.aborted) {
      return;
    }
    showError(error);
  } finally {
    if (activeController === controller) {
      activeController = null;
    }
  }
}

The identity check prevents an older request's cleanup from clearing the controller for a newer request. It also prevents stale results from replacing fresh ones if completion and user input race.

The string passed to abort() becomes the signal's reason in supporting implementations. Without a custom reason, cancellation commonly rejects with a DOMException named AbortError. Checking controller.signal.aborted is appropriate here because this code owns that exact signal. Do not classify every caught TypeError as cancellation; fetch uses TypeError for other failures too.

Browse Web APIs for patterns that keep interface state tied to the current operation.

Add a timeout without losing cleanup

A timeout is one policy for triggering the same controller:

async function fetchJsonWithTimeout(url, milliseconds) {
  const controller = new AbortController();
  const timer = setTimeout(() => {
    controller.abort(new Error("Request timed out"));
  }, milliseconds);

  try {
    return await fetchJson(url, { signal: controller.signal });
  } finally {
    clearTimeout(timer);
  }
}

Validate milliseconds before calling this helper; it should be a finite, non-negative number appropriate to the application. Clearing the timer in finally matters after success and every failure. Otherwise, a completed request leaves a callback waiting around to abort a controller nobody needs.

Modern runtimes also provide helpers such as AbortSignal.timeout() and signal composition in some environments. Check the actual browser or runtime support required by your project before replacing an explicit controller. A tutorial's “modern” and your embedded webview's “modern” may not have met.

Abort on component cleanup

When a view, component, or route starts a request, abort it during that view's cleanup. The framework-specific hook differs, but the ownership rule stays simple: the scope that starts the operation should arrange to stop or ignore it when that scope ends.

Aborting prevents your client from continuing to wait or consume a body. It does not guarantee that the server stopped processing, and it cannot reverse a database update or payment already accepted. Use idempotency, server-side cancellation, transaction design, or application-specific safeguards for consequential operations.

Test all four exits

Test successful JSON, an HTTP error, invalid JSON, and cancellation. Also start request A, immediately start request B, and confirm A cannot overwrite B. Use developer tools to slow the network rather than hoping your connection has an introspective afternoon.

Our optional-chaining guide explains the cleanup call activeController?.abort() and when that convenience would hide a missing requirement. Continue through language tools when the control flow is correct but the syntax still deserves inspection.

FAQ

How do I cancel a JavaScript fetch request?

Create an `AbortController`, pass `controller.signal` in the fetch options, and call `controller.abort()` from the relevant cancel or cleanup path. The fetch operation rejects if it has not completed, and body consumption can also be interrupted. Handle the rejection deliberately. Create a fresh controller for another request; once a signal is aborted, it remains aborted and cannot be reset.

Does fetch reject for a 404 or 500 response?

Normally, `fetch()` resolves to a `Response` when an HTTP response arrives, including error statuses such as 404 or 500. Check `response.ok` or inspect `response.status` and throw or return an application-specific result. Network failure, cancellation, an HTTP error response, and failure to parse the body are different outcomes. Keeping them distinct produces better retries, messages, logging, and tests.

Can I reuse an AbortController after aborting it?

No. Calling `abort()` permanently marks that controller's signal as aborted. Passing the same signal to a later operation causes it to begin in an aborted state. Create a new controller for every new request or operation. One live signal can intentionally control multiple operations that should cancel together, but aborting it cancels all participating operations and it still cannot be reset afterward.

Does aborting fetch cancel work on the server?

Not necessarily. Aborting tells the client to stop waiting for or consuming the operation, and the connection behavior may let a server notice disconnection. The server may already have received and processed the request. Cancellation cannot undo a database write, message, booking, or payment. Consequential operations need server-side safeguards such as idempotency, transactions, explicit cancellation support, or other application-specific design.

How should I implement a fetch timeout?

Use an abort signal triggered after a validated timeout, and clear the timer in `finally` after success, cancellation, or failure. Some current runtimes provide `AbortSignal.timeout()` and signal-composition helpers, but required browser or embedded-runtime support must be checked. Distinguish a timeout from user cancellation when the interface needs different messages. A timeout only ends the client's wait; it does not prove the server stopped processing.