How to Handle Errors When Making API Calls with Axios in React

Use a centralized axios instance with response interceptors to normalize network and server errors into domain-specific types, then handle them using try/catch blocks in your React components or delegate to React Error Boundaries.

When building React applications that depend on external APIs, implementing robust error handling is critical for production reliability. Every axios request returns a Promise that either resolves with response data or rejects with an error object shaped by the failure type. By combining axios interceptors with React's error management patterns—as demonstrated in the packages/react-devtools-shared/src/devtools/views/ErrorBoundary/ErrorBoundary.js file from the React source code—you can create a resilient data-fetching architecture.

Understanding the Axios Error Object Structure

Axios classifies failures into three distinct categories based on where the request breaks down. Recognizing these shapes lets you branch logic correctly in your handlers.

Network and No-Response Errors

When the request is transmitted but the server never responds—due to offline status, CORS blocks, or DNS failures—the error object contains the raw request instance at error.request (either an XMLHttpRequest or Node.js http.ClientRequest).

Server Response Errors (4xx/5xx)

When the server returns a status code outside the 2xx success range, axios populates error.response with the server's status, data payload, and headers.

Request Setup Errors

If the request fails before dispatch—due to invalid configuration or malformed URLs—the error contains only a descriptive string at error.message.

Creating a Centralized Axios Instance with Interceptors

Centralizing your axios configuration ensures consistent timeout settings, base URLs, and error transformation logic across your React application. Create a dedicated module at src/api/axios.js that exports a pre-configured instance.

// src/api/axios.js
import axios from 'axios';

// Create a reusable instance
const api = axios.create({
  baseURL: process.env.REACT_APP_API_URL,
  timeout: 10000,
});

// Central error interceptor
api.interceptors.response.use(
  response => response,
  error => {
    // Network error – no response received
    if (!error.response) {
      console.warn('Network error:', error);
      return Promise.reject({ 
        type: 'NETWORK', 
        message: 'Unable to reach server' 
      });
    }

    // Server responded with an error status
    const { status, data } = error.response;
    
    if (status === 401) {
      // Trigger token refresh or redirect to login
      // window.location.href = '/login';
    }

    // Return a normalized error object for downstream handling
    return Promise.reject({
      type: 'SERVER',
      status,
      message: data?.message ?? 'An error occurred',
      details: data,
    });
  },
);

export default api;

This interceptor inspects error.response to differentiate between network failures and HTTP error codes, normalizing them into predictable shapes that your React components can consume without parsing axios-specific internals.

Handling Errors in React Components

Consume the centralized instance inside your components using async/await with try/catch blocks. Map caught errors to local state for rendering UI feedback, following the error boundary patterns found in the React repository's packages/react-devtools-shared/src/devtools/views/ErrorBoundary/ErrorBoundary.js.

// src/components/UserProfile.jsx
import React, { useState, useEffect } from 'react';
import api from '../api/axios';

function UserProfile({ userId }) {
  const [profile, setProfile] = useState(null);
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const fetchProfile = async () => {
      try {
        const { data } = await api.get(`/users/${userId}`);
        setProfile(data);
      } catch (err) {
        // err follows the shape defined in the interceptor
        if (err.type === 'NETWORK') {
          setError('Network problem – try again later.');
        } else if (err.status === 404) {
          setError('User not found.');
        } else {
          setError(err.message);
        }
        console.error('API error', err);
      } finally {
        setLoading(false);
      }
    };
    
    fetchProfile();
  }, [userId]);

  if (loading) return <p>Loading…</p>;
  if (error)   return <p className="error">{error}</p>;

  return (
    <div>
      <h2>{profile.name}</h2>
      <p>{profile.email}</p>
    </div>
  );
}

export default UserProfile;

Alternative Pattern: Promise Chaining with .catch

For sequential requests or inline handling without async function wrappers, append a .catch() handler directly to the promise chain. The interceptor still normalizes the error before it reaches your handler.

api.post('/login', credentials)
  .then(res => {
    // Handle successful login
  })
  .catch(err => {
    // err already normalized by the interceptor
    alert(`Login failed: ${err.message}`);
  });

Summary

  • Network errors surface via error.request when the server never responds, while server errors populate error.response with status codes and data payloads.
  • Centralize axios configuration in src/api/axios.js to attach response interceptors that normalize errors into domain-specific types (NETWORK vs SERVER).
  • Inspect error.response.status inside interceptors to handle specific HTTP codes like 401 Unauthorized by triggering token refreshes or redirects.
  • Consume the configured instance in React components using async/await with try/catch blocks, mapping caught errors to local state for conditional UI rendering.
  • Refer to packages/react-devtools-shared/src/devtools/views/ErrorBoundary/ErrorBoundary.js in the React source code for examples of UI-level error boundaries that can catch rendering errors stemming from failed data fetches.

Frequently Asked Questions

What is the difference between error.request and error.response in axios?

error.request contains the raw XMLHttpRequest or Node.js http.ClientRequest object when a connection is established but no response is ever received (network failure). In contrast, error.response exists only when the server returns a status code outside the 2xx range, containing the HTTP status, headers, and response data.

How do I handle 401 Unauthorized errors globally in React?

Attach a response interceptor to your centralized axios instance that checks error.response.status === 401. Inside the interceptor, either redirect the user using window.location.href or trigger a silent token refresh before rejecting the promise, ensuring all API calls throughout your React app benefit from centralized authentication handling.

Should I use try/catch or .catch() for axios errors in React?

Use try/catch with async/await inside React components for readable sequential logic and clean local state updates. Reserve .catch() for promise chains when handling multiple sequential requests or when you need error handling without declaring an async function wrapper.

How can I implement a retry mechanism for failed axios requests?

Inside your response interceptor, check for specific error types (like error.type === 'NETWORK') or transient HTTP status codes (503, 504). Maintain a retry counter in the error config, then recursively re-invoke the failed request with exponential backoff before finally rejecting the promise after exhausting your maximum retry limit.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s https://instagit.com/install.md

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client