# TypeScript Fetch Requests: Best Practices for Response Types and Error Handling

> Master TypeScript fetch requests with best practices for response types and error handling. Learn to create robust, efficient, and maintainable API interactions.

- Repository: [Microsoft/TypeScript](https://github.com/microsoft/typescript)
- Tags: best-practices
- Published: 2026-02-16

---

**Wrap the native `fetch` API in a generic TypeScript function that validates `response.ok`, handles multiple body parsers based on `Content-Type`, and throws custom `HttpError` instances for centralized error handling.**

The Microsoft TypeScript repository ships with comprehensive type definitions for the browser Fetch API in its standard library, making it possible to build strongly-typed HTTP clients without external dependencies. When implementing **typescript fetch requests**, developers must address the gap between the generic `Promise<Response>` return type and the specific data shapes their applications expect. This guide demonstrates how to leverage the ambient DOM typings—found in [`src/lib/dom.generated.d.ts`](https://github.com/microsoft/TypeScript/blob/main/src/lib/dom.generated.d.ts)—to create a reusable, type-safe wrapper that handles JSON, text, binary responses, and HTTP error scenarios.

## Understand the Native Fetch Types in TypeScript

Before wrapping `fetch`, you must understand how the TypeScript compiler defines the API surface. The declarations are located in the generated DOM library file.

### The fetch Function Signature

According to [`src/lib/dom.generated.d.ts`](https://github.com/microsoft/TypeScript/blob/main/src/lib/dom.generated.d.ts) at line 43354, the global `fetch` function is declared as:

```typescript
declare function fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;

```

This signature reveals that `fetch` always returns a `Promise<Response>`, regardless of whether the server returns JSON, HTML, or binary data. **TypeScript cannot infer the specific payload type**, which necessitates a generic wrapper.

### RequestInit and Response Interfaces

The optional second argument conforms to the `RequestInit` interface defined at line 2206 in the same file. It includes properties such as `method`, `headers`, `body`, `mode`, `credentials`, and `signal` for cancellation.

The `Response` interface (line 29934) extends `Body` and provides:

- **Status checks**: `ok` (boolean), `status` (number), `statusText` (string)
- **Header access**: `headers` (Headers object)
- **Body consumption methods**: `json()`, `text()`, `blob()`, `arrayBuffer()`, `formData()`

These methods return promises that resolve to distinct types, requiring careful selection based on the expected `Content-Type`.

## Create a Type-Safe Fetch Wrapper

A robust wrapper bridges the gap between the generic `Response` and your application's domain models. The implementation below follows the five core practices: typed requests, status validation, content negotiation, network error handling, and cancellation support.

### Handling Multiple Response Types

Different endpoints return different payloads. Your wrapper should accept a generic type parameter `T` and select the correct parser based on either an explicit argument or the `Content-Type` header.

```typescript
/** Helper to infer response parsers from a MIME type */
function getParser<T>(type: string, resp: Response): Promise<T> {
  if (type.includes('application/json')) return resp.json() as Promise<T>;
  if (type.includes('text/')) return resp.text() as unknown as Promise<T>;
  if (type.includes('application/octet-stream')) return resp.arrayBuffer() as unknown as Promise<T>;
  // Fallback – raw Response object
  return Promise.resolve(resp as unknown as T);
}

```

### Implementing Custom HTTP Error Classes

When `response.ok` is false (status outside 200-299), you should throw a structured error containing the status code, URL, and attempted body parse. This centralizes error handling for downstream callers.

```typescript
/** Custom error to represent HTTP‑level failures */
export class HttpError extends Error {
  constructor(
    public readonly status: number,
    public readonly statusText: string,
    public readonly url: string,
    public readonly body: any,
  ) {
    super(`HTTP ${status} – ${statusText}`);
    this.name = 'HttpError';
  }
}

```

### Adding Timeout and Cancellation Support

The `RequestInit` interface accepts an optional `signal` property. By creating an `AbortController` and linking it to a `setTimeout`, you achieve automatic request cancellation after a deadline.

```typescript
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 15_000); // 15 seconds
const mergedInit = { ...(init ?? {}), signal: controller.signal };

```

## Complete Implementation Example

The following utility combines all best practices into a single, reusable `typedFetch` function. It works with the standard DOM library definitions found in `microsoft/TypeScript` and requires no external dependencies.

```typescript
export async function typedFetch<T = any>(
  input: RequestInfo | URL,
  init?: RequestInit,
  parser?: 'json' | 'text' | 'blob' | 'arrayBuffer',
): Promise<T> {
  // 1. Abort support with default timeout
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 15_000);
  const mergedInit = { ...(init ?? {}), signal: controller.signal };

  try {
    const resp = await fetch(input, mergedInit);
    clearTimeout(timeout);

    // 2. HTTP status validation
    if (!resp.ok) {
      let errBody: any = undefined;
      try {
        const ct = resp.headers.get('Content-Type') ?? '';
        errBody = await getParser<any>(ct, resp);
      } catch {
        // ignore parsing errors
      }
      throw new HttpError(resp.status, resp.statusText, String(input), errBody);
    }

    // 3. Select parser
    if (parser) {
      switch (parser) {
        case 'json': return (await resp.json()) as T;
        case 'text': return (await resp.text()) as unknown as T;
        case 'blob': return (await resp.blob()) as unknown as T;
        case 'arrayBuffer': return (await resp.arrayBuffer()) as unknown as T;
      }
    }

    // 4. Auto-detect from Content-Type
    const ct = resp.headers.get('Content-Type') ?? '';
    return await getParser<T>(ct, resp);
  } catch (e) {
    // 5. Network-level and abort handling
    if (e instanceof DOMException && e.name === 'AbortError') {
      throw new Error('Request timed out or was aborted');
    }
    throw e;
  }
}

// Usage examples
interface User {
  id: number;
  name: string;
  email: string;
}

// GET JSON
async function loadUser(id: number): Promise<User> {
  return await typedFetch<User>(`https://api.example.com/users/${id}`, {
    method: 'GET',
    headers: { Accept: 'application/json' },
  });
}

// POST with body
async function createUser(payload: Partial<User>): Promise<User> {
  return await typedFetch<User>('https://api.example.com/users', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Accept: 'application/json',
    },
    body: JSON.stringify(payload),
  }, 'json');
}

```

## Summary

- **Leverage ambient DOM types**: The `fetch`, `RequestInit`, and `Response` definitions in [`src/lib/dom.generated.d.ts`](https://github.com/microsoft/TypeScript/blob/main/src/lib/dom.generated.d.ts) provide the foundation for type-safe HTTP calls without extra dependencies.
- **Wrap for strong typing**: Because `fetch` returns `Promise<Response>`, use a generic wrapper function (`typedFetch<T>`) to cast the parsed body to your domain model.
- **Validate status codes**: Always check `response.ok` before parsing the body; throw a custom `HttpError` to unify error handling across your application.
- **Parse by Content-Type**: Use the `Content-Type` header to select between `json()`, `text()`, `blob()`, or `arrayBuffer()`, or allow callers to specify the parser explicitly.
- **Handle network and timeout errors**: Pass an `AbortSignal` from `AbortController` to `RequestInit.signal`, and wrap the call in `try … catch` to handle `DOMException` aborts and network failures.

## Frequently Asked Questions

### How do I type the JSON response body when using fetch in TypeScript?

Because the native `fetch` function returns `Promise<Response>` according to [`src/lib/dom.generated.d.ts`](https://github.com/microsoft/TypeScript/blob/main/src/lib/dom.generated.d.ts), you must explicitly cast the parsed result. Call `response.json()` and cast the result to your interface, or use a generic wrapper function like `typedFetch<T>(url, init)` that returns `Promise<T>`. This ensures that downstream code receives a strongly-typed object rather than `any`.

### What is the best way to handle HTTP error status codes in TypeScript fetch requests?

Check the `ok` property on the `Response` object, which is `false` for any status outside the 200–299 range. When `!response.ok`, throw a custom error class (e.g., `HttpError`) that captures `status`, `statusText`, and the attempted body parse. This centralizes error handling and allows calling code to catch a single, predictable error type rather than checking status codes manually at every call site.

### How can I implement request timeouts with fetch in TypeScript?

Create an `AbortController` instance and pass its `signal` property to the `RequestInit` object under the `signal` key. Then use `setTimeout` to call `controller.abort()` after your desired duration. According to the `RequestInit` interface in [`src/lib/dom.generated.d.ts`](https://github.com/microsoft/TypeScript/blob/main/src/lib/dom.generated.d.ts), the `signal` property is fully typed as `AbortSignal | null`. When aborted, `fetch` rejects with a `DOMException` named `AbortError`, which you can catch and re-throw as a timeout-specific error.