Skip to content
WANCODE
BM

Fetch with Timeout (AbortController)

Stop waiting for slow requests: abort fetch() after a time limit, support a cancel button, and tell the two apart.

fetch-with-timeout.js
/**
 * fetch() that gives up after `timeout` ms.
 * Also accepts your own AbortSignal (e.g. a "Cancel" button).
 */
export async function fetchWithTimeout(resource, { timeout = 8000, signal, ...options } = {}) {
  const controller = new AbortController();
  const timer = setTimeout(() => {
    controller.abort(new DOMException("Request timed out after " + timeout + " ms", "TimeoutError"));
  }, timeout);

  // Forward an outside signal into our controller
  const forwardAbort = () => controller.abort(signal.reason);
  if (signal) {
    if (signal.aborted) forwardAbort();
    else signal.addEventListener("abort", forwardAbort, { once: true });
  }

  try {
    return await fetch(resource, { ...options, signal: controller.signal });
  } finally {
    clearTimeout(timer);
    signal?.removeEventListener("abort", forwardAbort);
  }
}

fetch() has no timeout option. On a bad connection it can wait a very long time before failing. An AbortController gives you a signal you pass to fetch; calling controller.abort() makes that fetch reject immediately.

Line by line

  • { timeout = 8000, signal, ...options } pulls out our own options and keeps the rest (method, headers, body) to pass straight to fetch.
  • setTimeout aborts with a DOMException named TimeoutError, so callers can tell "too slow" apart from "the user cancelled" (AbortError).
  • An outside signal is forwarded into our controller, so both the timer and your cancel button can stop the same request.
  • finally always clears the timer and removes the listener — whether the request succeeded, failed or was aborted — so nothing leaks.

Two things fetch won't do for you

1) A 404 or 500 is not an error for fetch — check response.ok. 2) The timeout here covers waiting for the response headers; reading a large body afterwards (response.json()) isn't covered once the timer is cleared.

This helper

JavaScript
const res = await fetchWithTimeout("/api/items", {
  timeout: 5000,
  signal: cancelController.signal,
});

Built-in shortcut (newer browsers)

JavaScript
const res = await fetch("/api/items", {
  signal: AbortSignal.any([
    AbortSignal.timeout(5000),
    cancelController.signal,
  ]),
});

AbortSignal.timeout() and AbortSignal.any() do the same job natively. AbortSignal.any() is newer (2023–2024), so the helper is the safer choice if you support older browsers.

No live preview for this one: real network requests are switched off in the WANCode preview (see How your code runs), so a timeout can't be demonstrated honestly there. Try it in a local project.

Usage

JavaScript
import { fetchWithTimeout } from "./fetch-with-timeout.js";

async function loadItems() {
  try {
    const response = await fetchWithTimeout("/api/items", { timeout: 5000 });
    if (!response.ok) throw new Error("Server replied " + response.status);
    return await response.json();
  } catch (error) {
    if (error.name === "TimeoutError") {
      console.warn("The server is taking too long. Please try again.");
    } else if (error.name === "AbortError") {
      console.info("Request cancelled.");
    } else {
      console.error("Couldn't load items:", error.message);
    }
    return [];
  }
}