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

To handle errors with fetch in TypeScript, check response.ok yourself: a 404 or 500 normally fulfills the fetch() promise with a Response; it does not reject just because the HTTP status is an error. A reusable wrapper should separate request failures, non-success HTTP responses, body parsing failures, and cancellation—and should not claim that a TypeScript type validates JSON at runtime.

Why doesn’t fetch throw on 404?

fetch() rejects when the request itself cannot be completed, such as for some network failures or an invalid URL scheme. An HTTP response with status 404 or 500 is still a response, so the promise normally fulfills. That means try/catch alone does not detect unsuccessful HTTP statuses. The [MDN Fetch guide](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) describes this distinction.

Fetch rejection, HTTP status, response-body decoding, and cancellation are separate failure points. Keeping them distinguishable lets callers decide what to show, whether an outcome is expected, or whether a retry is appropriate. Fetch itself does not prescribe this error taxonomy; it is a wrapper design choice.

How do I check whether a fetch response is OK?

Check response.ok immediately after awaiting Fetch. It is true for status codes from 200 through 299, as defined by [MDN’s Response.ok reference](https://developer.mozilla.org/en-US/docs/Web/API/Response/ok). A strict 2xx rule is a practical default, but some APIs assign meaning to statuses outside that range, such as 304. For those endpoints, define an explicit status policy rather than treating every non-2xx response as an unexpected failure.

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

How do I make a reusable fetch wrapper?

A small two-layer design keeps HTTP policy separate from decoding: one function returns a checked raw Response, while convenience functions parse JSON or text. The following example assumes a browser or worker with Fetch, or Node.js 18 or later with global Fetch. Node.js documents global Fetch as available from v18 and no longer experimental in v21; see the [Node.js v24.2.0 global objects documentation](https://nodejs.org/docs/latest-v24.x/api/globals.html). Check the runtime documentation if targeting older Node versions.

Layer 1: request and enforce the HTTP policy

export class HttpError extends Error {
  constructor(
    message: string,
    public readonly status: number,
    public readonly response: Response,
  ) {
    super(message);
    this.name = "HttpError";
  }
}

export async function request(
  input: RequestInfo | URL,
  init?: RequestInit,
): Promise<Response> {
  const response = await fetch(input, init);

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

  return response;
}

The original Fetch rejection is not converted into an HttpError; it remains a request-level failure. The HTTP error retains the response so callers can inspect its status, headers, or body if appropriate. Because the body is a stream, reading it for an error message consumes it; clone the response before reading if another part of the program must also read that body.

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

Layer 2: parse data explicitly

export async function requestJson(
  input: RequestInfo | URL,
  init?: RequestInit,
): Promise<unknown> {
  const response = await request(input, init);
  return response.json();
}

export async function requestText(
  input: RequestInfo | URL,
  init?: RequestInit,
): Promise<string> {
  const response = await request(input, init);
  return response.text();
}

Returning unknown from the JSON helper is honest: valid JSON syntax does not prove that the value matches an application interface. TypeScript’s unknown requires callers to narrow the value before using it, unlike any, which permits unchecked operations. See the [TypeScript Handbook’s basic types reference](https://www.typescriptlang.org/docs/handbook/2/basic-types.html).

If your API contract is trusted and you want a convenient generic signature, you can return Promise<T> and cast the result of response.json() to T. That cast is only a compile-time assertion; it does not validate the server payload. For untrusted or contract-sensitive data, validate unknown with a schema or type guard, and let invalid JSON surface as a decoding/parsing failure rather than an HTTP status error.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

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

What should the wrapper do about cancellation?

Pass the caller’s signal through RequestInit; do not replace or drop it. Fetch can be aborted while the request is in progress or while its response body is being read, and an abort rejects with an AbortError. The MDN Fetch guide documents cancellation and body-read behavior. Since cancellation is distinct from a network or HTTP failure, callers can recognize it and avoid presenting it as an ordinary server error.

const controller = new AbortController();

try {
  const data = await requestJson("/api/profile", {
    signal: controller.signal,
  });
  // Narrow or validate data before relying on its shape.
} catch (error: unknown) {
  if (error instanceof HttpError) {
    console.error("HTTP status:", error.status);
  } else if (error instanceof Error && error.name === "AbortError") {
    // The request or body read was cancelled.
  } else {
    // Request-level or decoding failure; inspect and handle deliberately.
  }
}

Catch values as unknown and narrow them before reading properties. Depending on the runtime and Fetch implementation, a request-level rejection need not provide a useful application-specific error class, so avoid assuming every caught value is an Error.

Which wrapper design should you choose?

Choice Useful when Trade-off
Raw Response or parsed data Return a raw response when callers need headers, status, or control of body reading; use parsed helpers for convenience. Parsing consumes the body stream. A response body normally cannot be read twice unless it is cloned before consumption.
Throwing or result union Throwing composes naturally with async/await; a discriminated union makes expected outcomes explicit in ordinary return values. A result union changes how every caller handles success and failure; neither approach is universally best.
Strict 2xx or configurable status policy Use response.ok for a simple 2xx default; configure endpoint-specific accepted statuses when the API treats other statuses as meaningful. A strict default may classify a meaningful non-2xx outcome as an error.
Generic cast or runtime validation A generic return type is concise for trusted contracts; runtime validation is appropriate when payload shape must be checked. A cast provides no runtime guarantee. Validation requires an explicit schema or type guard.
Global or injected Fetch Global Fetch is straightforward for browser and supported Node.js environments; injection can help isolated tests and alternate Fetch-compatible implementations. Injection is a design option, not a requirement of the Fetch API.

How should callers handle each failure category?

  • Request rejection: Fetch did not produce a usable HTTP response. Surface or log the failure with request context; do not label it an HTTP status error.
  • Non-success HTTP response: Inspect the status and, if needed, response headers or body. Whether to retry depends on method idempotency, server behavior, and application requirements; do not automatically retry every failure.
  • Body decoding or parsing failure: Treat malformed JSON or a failed body read separately from the HTTP status. The response may have had a successful status even though its payload could not be consumed as expected.
  • Abort: Recognize cancellation distinctly. It may reflect a user action or an application timeout, not a server failure.

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.