Skip to content
Commitpedia
Log in Join & edit

fetch with timeout, retries and proper error handling

A reusable fetchJson helper for Node 20+ and browsers that adds a timeout, retries with backoff for temporary failures, and clear errors for HTTP status codes.

Not checked by a trusted editor yet Last edited Outdated?

Edit history of fetch with timeout, retries and proper error handling: 1 recorded edit
Edit history · 1 edit
Type
Script
Edits
1
Editors
1
First issued
15 Oct 2026
Last edit
15 Oct 2026
Language
javascript
Licence
CC0-1.0

What this changes

Read-only: it sends GET requests (the first try plus up to the given number of retries) to the URL you pass and prints the JSON answer; nothing on your system is changed.

How to undo it

Nothing to undo.

Plain fetch has two surprises: it has no default timeout, and it does not reject on HTTP errors like 404 or 500. This helper fixes both and retries only the failures that are worth retrying.

What it handles

  • Timeouts via AbortSignal.timeout(ms), which aborts the request with a TimeoutError.
  • HTTP errors: checks response.ok and throws an HttpError carrying the status and body text.
  • Retries with exponential backoff (250 ms, 500 ms, 1 s…) for timeouts, network failures and 5xx responses. A 4xx response is your bug or the user’s, so it is not retried.
  • Cancellation: pass your own signal; it is combined with the timeout using AbortSignal.any(), and a manual abort is never retried.

The script

Save as fetch-json.mjs. It works as a module you import, and as a small command-line tool. Requires Node 20.3 or later, or a current browser if you remove the CLI block at the bottom.

#!/usr/bin/env node
// Usage: node fetch-json.mjs <url> [timeoutMs] [retries]
import { fileURLToPath } from "node:url";

export class HttpError extends Error {
  constructor(response, body) {
    super(`HTTP ${response.status} ${response.statusText} for ${response.url}`);
    this.name = "HttpError";
    this.status = response.status;
    this.body = body;
  }
}

export async function fetchJson(url, { timeoutMs = 5000, retries = 2, signal, ...init } = {}) {
  for (let attempt = 0; ; attempt++) {
    const timeout = AbortSignal.timeout(timeoutMs);
    const combined = signal ? AbortSignal.any([signal, timeout]) : timeout;
    try {
      const response = await fetch(url, {
        ...init,
        signal: combined,
        headers: { accept: "application/json", ...init.headers },
      });
      if (!response.ok) {
        throw new HttpError(response, await response.text().catch(() => ""));
      }
      return await response.json();
    } catch (err) {
      const retryable =
        err.name === "TimeoutError" ||
        err.name === "TypeError" ||
        (err instanceof HttpError && err.status >= 500);
      if (signal?.aborted || !retryable || attempt >= retries) throw err;
      const delay = 250 * 2 ** attempt;
      console.error(`Attempt ${attempt + 1} failed (${err.message}), retrying in ${delay} ms`);
      await new Promise((r) => setTimeout(r, delay));
    }
  }
}

if (import.meta.main ?? process.argv[1] === fileURLToPath(import.meta.url)) {
  const [url, timeoutMs = "5000", retries = "2"] = process.argv.slice(2);
  if (!url) {
    console.error("Usage: node fetch-json.mjs <url> [timeoutMs] [retries]");
    process.exit(2);
  }
  try {
    const data = await fetchJson(url, { timeoutMs: Number(timeoutMs), retries: Number(retries) });
    console.log(JSON.stringify(data, null, 2));
  } catch (err) {
    console.error(`${err.name}: ${err.message}`);
    process.exit(1);
  }
}

Usage

node fetch-json.mjs https://api.github.com/repos/nodejs/node 3000 2
import { fetchJson, HttpError } from "./fetch-json.mjs";

try {
  const repo = await fetchJson("https://api.github.com/repos/nodejs/node", { timeoutMs: 3000 });
  console.log(repo.full_name);
} catch (err) {
  if (err instanceof HttpError && err.status === 404) console.log("No such repo");
  else throw err;
}

How the error types map

  • TimeoutError: the timeout signal fired.
  • AbortError: your own signal was aborted (for example the user navigated away).
  • TypeError: fetch failed: DNS failure, refused connection or similar network problem. In Node the underlying reason is in err.cause.
  • HttpError: the server answered, but with a non-2xx status.
  • SyntaxError: the body was not valid JSON. Not retried, because repeating the request rarely helps.

Notes

  • Only retry requests that are safe to repeat. GET, PUT and DELETE are designed to be idempotent; a retried POST might create a duplicate order. Pass retries: 0 for those.
  • For 429 and 503 responses, a production version should honor the Retry-After header instead of a fixed backoff.
  • The timeout covers the whole call, including reading the body, because the same signal stays attached while response.json() runs.

See also

  • 0018 Promises and async/await explained A promise is a placeholder for a value that arrives later. async/await lets you write code that waits for promises as if… Intermediate · Concept · Any system Article Async code 15 Oct 2026
  • 0023 Run async tasks with a concurrency limit A small mapLimit helper that runs an async function over many items with at most N in flight, keeps result order, stops… Advanced · How-to · Any system Script Async code 15 Oct 2026
  • 0081 How an HTTP request works: methods, headers, and body An HTTP request is a short text message a client sends to a server: a method and path, some headers, and sometimes… Beginner · Concept · Any system Article Making requests 15 Oct 2026
  • 0083 Test a JSON API with curl A runnable bash script showing the curl commands you need most when testing an API: GET, POST JSON, custom headers, Bearer tokens,… Beginner · How-to · Linux, macOS, Windows Script Authentication · Making requests 15 Oct 2026
  • 0084 Tiny JSON echo server for testing HTTP clients A standard-library Python server that echoes back the method, path, query, headers, and body of every request, and can return any status… Intermediate · How-to · Any system Script Files & automation · Making requests 15 Oct 2026
  • 0085 fetch with timeout, retries, and exponential backoff A drop-in fetch wrapper that times out slow requests, retries network errors and 429/5xx responses with jittered exponential backoff, and honors Retry-After. Advanced · How-to · Any system, Browser Script Async code · Making requests 15 Oct 2026

Repos in the wild

  • Requests: HTTP for humans psf/requests Requests is the classic Python library for calling web APIs and downloading pages, with a simple API for GET and POST requests, headers, JSON and sessions. GitHub
  • Axios axios/axios A promise-based HTTP client for browsers and Node.js with interceptors, automatic JSON handling, request cancellation and a consistent API in both environments. GitHub
  • curl and libcurl (curl/curl) curl/curl The source of curl, the command-line tool for transferring data with URLs, and libcurl, the C library behind it that many other programs use for HTTP. GitHub

History

Every saved edit is kept. The plot on the sleeve draws one line per edit. Open any edit to see exactly what changed.

  1. Commitpedia +4,243 chars

Report a problem

Something wrong, unsafe or copied? Tell the editors.
What is wrong?