Centralized Error Handling with Axios Interceptors

This article explains how to implement centralized error handling in modern web applications using Axios response interceptors. By capturing HTTP responses before they reach individual application components, developers can manage network failures, authentication issues, and server-side errors from a single, centralized location. You will learn the core concepts of response interceptors, how to configure a reusable Axios instance, and how to handle specific HTTP status codes such as 401 Unauthorized and 500 Internal Server Error efficiently.

Understanding Axios Interceptors

Axios interceptors are middleware-like functions that allow you to inspect or modify HTTP requests and responses globally before they are handled by then or catch blocks.

A response interceptor accepts two callback functions:

  1. Success Handler: Executed when the API returns an HTTP status code in the 2xx range.
  2. Error Handler: Executed when the request fails, the network drops, or the server returns an HTTP status code outside the 2xx range (e.g., 4xx or 5xx).

By intercepting errors globally, you eliminate the need to write repetitive try...catch blocks across every API call in your codebase.

Setting Up a Custom Axios Instance

The best practice for implementing interceptors is to attach them to a custom Axios instance rather than the global axios object. This keeps configurations modular and prevents unintended side effects across third-party libraries.

// apiClient.js
import axios from 'axios';

const apiClient = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json',
  },
});

export default apiClient;

Implementing the Centralized Response Interceptor

You can attach the interceptor to the instance using the apiClient.interceptors.response.use() method.

// apiClient.js
import axios from 'axios';

const apiClient = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json',
  },
});

// Response Interceptor
apiClient.interceptors.response.use(
  (response) => {
    // Return the response data directly for successful requests
    return response;
  },
  (error) => {
    // Centralized error handling logic
    handleGlobalErrors(error);

    // Reject the promise so local callers can still catch errors if necessary
    return Promise.reject(error);
  }
);

function handleGlobalErrors(error) {
  if (error.response) {
    // The server responded with a status code outside the 2xx range
    const { status, data } = error.response;

    switch (status) {
      case 400:
        console.error('Bad Request:', data.message || 'Invalid input.');
        break;
      case 401:
        console.error('Unauthorized: Redirecting to login...');
        // Clear auth tokens and redirect user
        localStorage.removeItem('authToken');
        window.location.href = '/login';
        break;
      case 403:
        console.error('Forbidden: You do not have permission to perform this action.');
        break;
      case 404:
        console.error('Not Found: The requested resource does not exist.');
        break;
      case 500:
      case 502:
      case 503:
        console.error('Server Error: Please try again later.');
        break;
      default:
        console.error(`Unhandled Error (${status}):`, data.message || 'An error occurred.');
    }
  } else if (error.request) {
    // The request was made, but no response was received (e.g., network timeout)
    console.error('Network Error: Unable to reach the server. Please check your connection.');
  } else {
    // Something happened while setting up the request
    console.error('Request Setup Error:', error.message);
  }
}

export default apiClient;

Handling Token Refresh Automatically

A primary use case for centralized error interceptors is handling expired authentication tokens without disrupting the user experience. When a 401 Unauthorized response is detected, the interceptor can request a new token and retry the original failed request seamlessly.

let isRefreshing = false;
let failedQueue = [];

const processQueue = (error, token = null) => {
  failedQueue.forEach((prom) => {
    if (error) {
      prom.reject(error);
    } else {
      prom.resolve(token);
    }
  });
  failedQueue = [];
};

apiClient.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;

    // Check if error is 401 and request has not already been retried
    if (error.response?.status === 401 && !originalRequest._retry) {
      if (isRefreshing) {
        // Queue the request while token refresh is in progress
        return new Promise((resolve, reject) => {
          failedQueue.push({ resolve, reject });
        })
          .then((token) => {
            originalRequest.headers['Authorization'] = `Bearer ${token}`;
            return apiClient(originalRequest);
          })
          .catch((err) => Promise.reject(err));
      }

      originalRequest._retry = true;
      isRefreshing = true;

      try {
        const refreshToken = localStorage.getItem('refreshToken');
        const { data } = await axios.post('https://api.example.com/auth/refresh', {
          token: refreshToken,
        });

        const newAccessToken = data.accessToken;
        localStorage.setItem('accessToken', newAccessToken);

        apiClient.defaults.headers.common['Authorization'] = `Bearer ${newAccessToken}`;
        originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`;

        processQueue(null, newAccessToken);
        return apiClient(originalRequest);
      } catch (refreshError) {
        processQueue(refreshError, null);
        localStorage.clear();
        window.location.href = '/login';
        return Promise.reject(refreshError);
      } finally {
        isRefreshing = false;
      }
    }

    return Promise.reject(error);
  }
);

Key Benefits