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.

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

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.
TYPESCRIPTasync 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

- Define a new interface called
ValidationErrorthat extendsApiErrorResponseto include anerrorsobject (a mapping of field names to error messages). - Update the
requestfunction to check if the status is422 Unprocessable Entity. - Create a utility function
isApiError(err: unknown): err is ApiErrorto clean up yourcatchblocks.
Common Pitfalls
- Ignoring
unknown: Don't usecatch (error: any). It disables type checking for that block. Useinstanceofor 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 theContent-Typeheader. - Over-logging: Be careful not to log sensitive details from the
ApiErrorto 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

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.
Work with me

Next.js Full-Stack Web App Development
A fast, SEO-ready full-stack web app built with Next.js 16 — from idea to deployed product, by an engineer who ships to production.

Laravel REST API Development
Clean, secure, well-documented Laravel REST APIs — the backend engine for your app, mobile client, or SaaS. Built by an API specialist.


