Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use an abort signal to put a time limit on each Fetch attempt, then add a finite retry loop that treats HTTP responses separately from rejected requests. Create a fresh signal for every attempt, and retry only when the operation is safe to repeat. A client-side timeout does not prove the server did not receive or process the request.

What a Fetch timeout does—and does not—mean

Fetch accepts an AbortSignal. Aborting that signal rejects an in-flight fetch with an AbortError; aborting after the response headers arrive can also cause a later read of the response body to reject. See MDN’s guide to canceling a Fetch request.

A timeout is a limit on how long the client waits, not evidence that the server never received or completed the operation. If a request might have changed server state, blindly retrying after a timeout could perform that action twice. Only retry when the operation is safe to replay, or when the API provides an idempotency mechanism that lets you safely repeat it.

Choose how to create the timeout signal

Use AbortSignal.timeout() in supported runtimes

For a single request, pass AbortSignal.timeout(timeoutMs) as the signal:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const response = await fetch(url, {
  signal: AbortSignal.timeout(timeoutMs),
});

The timeout signal aborts with a TimeoutError DOMException. Its clock measures active time, so suspension of a page or worker—and time in the back-forward cache—can affect when it fires. MDN marks the method Baseline 2024 and notes that older browsers may not support it. It also does not expose a way to cancel its timer early; use an AbortController and a cancellable timer when that matters. See MDN’s AbortSignal.timeout() reference.

Combine a timeout with caller cancellation

If the caller can cancel the request, combine its signal with a timeout using AbortSignal.any() where supported:

Rank #2
TypeScript Programming Language - Software Engineer & Coder T-Shirt
  • TypeScript implements a superset of syntax for strictly typed development, facilitating deep static analysis and enhanced development environment integration. The compiler translates source into standard script formats, ensuring parity across any runtime.
  • TypeScript is ideal for front-end developers, full-stack engineers, and software architects who build large-scale web applications. It serves those looking to improve code excellence, reduce bugs through static checking, and maintain complex projects more.
  • Lightweight, Classic fit, Double-needle sleeve and bottom hem
const signal = AbortSignal.any([
  callerSignal,
  AbortSignal.timeout(timeoutMs),
]);

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

The combined signal’s abort reason is available, but the helper does not provide a separate built-in flag identifying which input signal triggered it. Inspect the reason or error name, and make sure caller cancellation is not mistaken for a transient failure eligible for retry. See MDN’s AbortSignal.any() reference.

Use a controller and cancellable timer when needed

For older targets, or when you want to clear the timeout timer explicitly, create an AbortController for the attempt. Start a setTimeout() timer that calls abort() before invoking Fetch, pass the controller’s signal, and call clearTimeout() in a finally block. If you also accept a caller signal, attach a one-shot abort listener that aborts the per-attempt controller, then remove the listener in finally. This lets both events cancel the same attempt while ensuring the timer and listener are cleaned up.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Distinguish HTTP errors from rejected requests

Fetch usually fulfills with a Response even for HTTP error statuses such as 404. Check response.ok or response.status to decide whether the server response represents success. A rejected Fetch promise generally indicates a network or request failure, not an HTTP status. Keep these cases in separate branches: your application’s status policy belongs after Fetch resolves, while network failures and aborts belong in the catch path. See MDN’s Fetch response-status guidance.

Build a finite retry loop with a fresh signal per attempt

An aborted signal cannot be reused for a live attempt. Create a new controller or timeout signal on every pass through the loop. The following is a teaching skeleton, not a drop-in retry policy; define its helpers to match the API you are calling:

async function fetchWithRetry(
  input: RequestInfo | URL,
  init: RequestInit = {},
  options: { timeoutMs: number; maxRetries: number },
): Promise<Response> {
  let lastError: unknown;
  const callerSignal = init.signal;

  for (let attempt = 0; attempt <= options.maxRetries; attempt++) {
    const controller = new AbortController();
    const timer = setTimeout(
      () => controller.abort(),
      options.timeoutMs,
    );
    const onCallerAbort = () => controller.abort(callerSignal?.reason);

    if (callerSignal?.aborted) onCallerAbort();
    else callerSignal?.addEventListener("abort", onCallerAbort, { once: true });

    try {
      const response = await fetch(input, {
        ...init,
        signal: controller.signal,
      });

      if (response.ok) return response;
      if (!shouldRetryStatus(response.status) || attempt === options.maxRetries) {
        return response;
      }

      // Release the response before attempting the request again.
      await response.body?.cancel();
    } catch (error) {
      lastError = error;
      if (
        callerSignal?.aborted ||
        isTimeoutOrAbort(error) ||
        attempt === options.maxRetries
      ) {
        throw error;
      }
      // Retry only if this failure is transient and replaying is safe.
    } finally {
      clearTimeout(timer);
      callerSignal?.removeEventListener("abort", onCallerAbort);
    }

    await delay(backoffWithJitter(attempt));
  }

  throw lastError;
}

The loop counts maxRetries retries in addition to the initial attempt. Its status check and catch branch are intentionally distinct: decide which HTTP statuses are retryable for the service, and separately classify rejected requests. Do not retry caller cancellation. If a response will not be retried, return it for the caller to inspect; if it will be retried, consume or cancel its body first.

The placeholders shouldRetryStatus, isTimeoutOrAbort, delay, and backoffWithJitter represent application policy, not built-in Fetch functions. The available Fetch and abort documentation does not establish one universal status list, retry count, timeout duration, or backoff formula.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Decide whether an operation can safely be retried

Before enabling retries, check all of the following against the service contract and the request you are sending:

  • Operation safety: Can repeating this method and operation cause duplicate side effects? A timeout cannot settle whether the server processed the first attempt.
  • Failure classification: Which network failures and HTTP statuses are transient for this API? Treat status codes and promise rejections as separate cases.
  • Request-body replayability: Can the same body be sent again? A consumed stream or one-shot body may not be reusable; constructing a new request or body for each attempt may be necessary.
  • Response cleanup: If retrying a resolved HTTP error, decide whether to consume or cancel its body before continuing.
  • Server guidance: Honor any applicable server retry limits or timing guidance, including Retry-After, if the API contract calls for it.
  • Latency budget: Bound both the attempts and their delays so the retry process fits the caller’s total time budget.
  • Observability: Record attempts and their outcomes so repeated failures and timeouts can be diagnosed.

Check TypeScript and runtime support

Runtime support and TypeScript declarations are separate questions. Node.js v26.8.2 documentation lists global Fetch and AbortSignal.timeout(), but a project must target a deployed Node version that actually provides the APIs and use library/type declarations compatible with its environment. Browser, Node, worker, and mixed projects can differ, so no single tsconfig setting is universal. Check the documentation for the runtime version you deploy; see Node.js v26.8.2 global Fetch documentation.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.