Back to Blog
Lesson 32 of the TypeScript: Typing JavaScript with Confidence course
TypeScriptAugust 19, 20264 min read

Handling API Errors: A Type-Safe Approach in TypeScript

Master API Error Handling in TypeScript. Learn to create custom error interfaces, manage HTTP status codes, and implement type-safe catch blocks for your app.

TypeScriptAPIError HandlingFetchWeb Development
Close-up of PHP code on a monitor, highlighting development and programming concepts.

Previously in this course, we built Creating a Generic Request Wrapper in TypeScript. Now that we have a standard way to send requests, we need to handle the reality of the web: things will go wrong.

In production, "error handling" isn't just about try/catch blocks; it's about providing the consuming code with enough type information to decide whether to retry a request, show a validation message, or log a fatal crash.

Why Standardize Error Handling?

When you use fetch, a request only "fails" in the catch block if a network error occurs (e.g., DNS failure). If the server returns a 404 Not Found or a 500 Internal Server Error, fetch considers the promise "resolved."

If you don't handle these statuses explicitly, your application will attempt to parse broken or empty JSON, leading to cryptic errors later in your component logic. We need to bridge this gap between HTTP status codes and our TypeScript types.

Creating Type-Safe Error Interfaces

Close-up of a computer screen displaying an authentication failed message.

Instead of throwing raw strings or generic Error objects, we should define a structured shape for our API errors. This allows developers to access properties like code or message without casting types.

TYPESCRIPT
// types/api.ts
export interface ApiErrorResponse {
  message: string;
  code: string;
  details?: Record<string, unknown>;
}

export class ApiError extends Error {
  constructor(
    public readonly status: number,
    public readonly data: ApiErrorResponse
  ) {
    super(data.message);
    this.name = CE9178">'ApiError';
  }
}

By extending the native Error class, we maintain compatibility with existing tools while adding the status code and our typed ApiErrorResponse payload.

Handling HTTP Status Codes

We need to update our request wrapper to inspect the response.ok property. If it's false, we know we need to extract the error body and throw our custom ApiError.

TYPESCRIPT
// api/client.ts
import { ApiError, ApiErrorResponse } from CE9178">'../types/api';

async function request<T>(url: string, options: RequestInit): Promise<T> {
  const response = await fetch(url, options);

  if (!response.ok) {
    // Attempt to parse the error response from the server
    const errorData: ApiErrorResponse = await response.json().catch(() => ({
      message: CE9178">'An unknown error occurred',
      code: CE9178">'UNKNOWN_ERROR'
    }));
    
    throw new ApiError(response.status, errorData);
  }

  return response.json();
}

Implementing Type-Safe Error Catching

When consuming this API client, you will likely use a try/catch block. In TypeScript, the error caught in a catch block is typed as unknown by default. We must use a type guard to safely access our custom properties.

TYPESCRIPT
async function fetchTask(id: string) {
  try {
    const task = await request<Task>(CE9178">`/tasks/${id}`, { method: CE9178">'GET' });
    console.log(task);
  } catch (error) {
    if (error instanceof ApiError) {
      // TypeScript now knows CE9178">'error' has a CE9178">'status' property
      if (error.status === 404) {
        console.error(CE9178">'Task not found:', error.data.message);
      }
    } else {
      console.error(CE9178">'Unexpected non-API error:', error);
    }
  }
}

Practice Exercise

Adults in a yoga studio stretch on mats, promoting fitness and flexibility.

  1. Define a new interface called ValidationError that extends ApiErrorResponse to include an errors object (a mapping of field names to error messages).
  2. Update the request function to check if the status is 422 Unprocessable Entity.
  3. Create a utility function isApiError(err: unknown): err is ApiError to clean up your catch blocks.

Common Pitfalls

  • Ignoring unknown: Don't use catch (error: any). It disables type checking for that block. Use instanceof or a type guard instead.
  • Forgetting JSON parsing errors: If your server returns a non-JSON error (like a plain text 502 Bad Gateway), response.json() will throw. Always wrap your error parsing in a .catch() block or check the Content-Type header.
  • Over-logging: Be careful not to log sensitive details from the ApiError to the console in production environments.

FAQ

Why extend Error instead of just returning an object? Extending Error gives you the stack trace automatically, which is invaluable for debugging issues in production environments.

How does this differ from REST API Error Handling: Standardizing with RFC 7807? RFC 7807 provides a global standard for the server response format. Our ApiError class is the client-side implementation that consumes those standard responses.

Recap

Team members presenting a project in a modern office setting with a focus on collaboration.

We've moved from basic fetching to robust API interaction. By defining ApiError and using type guards, we ensure our application can react gracefully to different failure scenarios, keeping our UI logic clean and our data predictable.

Up next: Query Parameters with Generics where we will make our client even more flexible by adding support for dynamic filtering and sorting.

Similar Posts