Custom Error Types in Axios TypeScript Setup

Handling API errors with proper type safety is essential for robust TypeScript applications. This guide demonstrates the syntax and patterns required to define and handle custom error types when using Axios in a TypeScript setup, enabling you to capture strongly typed backend error payloads, status codes, and application-specific exceptions.

1. Define the API Error Response Payload

First, define a TypeScript interface or type that represents the JSON structure returned by your backend when an error occurs.

export interface ApiErrorPayload {
  status: number;
  message: string;
  code?: string;
  errors?: Record<string, string[]>;
}

2. Using AxiosError with Generics

Axios provides the generic AxiosError<T> type, where T corresponds to the response body type. You can type-alias this directly:

import { AxiosError } from 'axios';

export type CustomAxiosError = AxiosError<ApiErrorPayload>;

3. Creating a Custom Error Class

To encapsulate error data into a standardized domain object, extend JavaScript's built-in Error class:

export class ApiError extends Error {
  public readonly statusCode: number;
  public readonly errorCode?: string;
  public readonly validationErrors?: Record<string, string[]>;

  constructor(message: string, statusCode: number, errorCode?: string, validationErrors?: Record<string, string[]>) {
    super(message);
    this.name = 'ApiError';
    this.statusCode = statusCode;
    this.errorCode = errorCode;
    this.validationErrors = validationErrors;

    // Restore prototype chain for instanceof checks
    Object.setPrototypeOf(this, ApiError.prototype);
  }
}

4. Transforming Errors with an Axios Interceptor

Use Axios interceptors to catch raw network errors, extract typed backend payloads, and throw your custom error class.

import axios, { AxiosInstance, AxiosError } from 'axios';

const apiClient: AxiosInstance = axios.create({
  baseURL: 'https://api.example.com',
});

apiClient.interceptors.response.use(
  (response) => response,
  (error: AxiosError<ApiErrorPayload>) => {
    if (error.response) {
      // Backend returned an error response (4xx, 5xx)
      const { data, status } = error.response;
      throw new ApiError(
        data.message || 'An error occurred',
        status,
        data.code,
        data.errors
      );
    } else if (error.request) {
      // Request was made but no response was received (Network error)
      throw new ApiError('Network error: No response from server', 0);
    } else {
      // Request configuration issue
      throw new ApiError(error.message, -1);
    }
  }
);

export default apiClient;

5. Type Narrowing with Type Guards

When catching errors in try/catch blocks, TypeScript types caught variables as unknown. Use type guards to narrow them to your custom types:

import axios from 'axios';

async function fetchUserData(userId: string) {
  try {
    const response = await apiClient.get(`/users/${userId}`);
    return response.data;
  } catch (error: unknown) {
    if (error instanceof ApiError) {
      console.error(`API Error [${error.statusCode}]:`, error.message);
      if (error.validationErrors) {
        console.error('Validation details:', error.validationErrors);
      }
    } else if (axios.isAxiosError<ApiErrorPayload>(error)) {
      console.error('Direct Axios Error:', error.response?.data.message);
    } else {
      console.error('Unexpected Error:', error);
    }
  }
}