Centralizing Error Handling and Logging with Axios Interceptors
Learn how to use Axios request and response interceptors to inject auth tokens, log calls, and normalize errors across a JavaScript app, plus the trade‑offs to watch for.
05 Jul 2026, 16:37 UTC

The Problem: Repetitive Error‑Handling Code
When building a frontend that talks to many REST endpoints, developers often end up copying the same boilerplate:
- Attach an Authorization header on every request.
- Log the request URL and method for debugging.
- Catch network errors, transform non‑2xx responses into a uniform error shape, and send them to a reporting service.
- Set a timeout so a stalled request doesn’t hang the UI.
Doing this manually for each axios.get or axios.post call creates noise, increases the chance of inconsistencies, and makes future changes (e.g., switching token storage) a tedious hunt‑and‑replace.
Thesis: Use Axios Interceptors for a Single Source of Truth
Axios provides request and response interceptors that run for every HTTP call made through an instance. By placing shared logic in these interceptors, you keep the calling code clean and guarantee that concerns like authentication, logging, and error normalization are applied uniformly.
1. Request Interceptor – Token Injection & Logging
Create an Axios instance (or configure the default one) and attach a request interceptor that:
- Reads the auth token from wherever you store it (e.g.,
localStorageor a state manager). - Adds it as an
Authorizationheader. - Logs the outgoing request details to the console (or a custom logger).
// src/api/axiosInstance.js
import axios from 'axios';
const api = axios.create({
baseURL: process.env.REACT_APP_API_URL,
timeout: 8000, // global timeout in milliseconds
});
// Request interceptor
api.interceptors.request.use(config => {
const token = localStorage.getItem('accessToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// Light‑weight logging – avoid heavy payloads
console.debug(`[Axios] ${config.method.toUpperCase()} ${config.url}`);
return config;
}, error => {
// Do not intercept request errors here; let them propagate
return Promise.reject(error);
});
// Export the configured instance
export default api;
Where to run: This file is imported once at application startup (e.g., in index.js or a root component). No special permissions are needed beyond normal frontend execution.
2. Response Interceptor – Error Normalization & Logging
The response interceptor catches both successful responses and errors. Here we:
- Log the status code and response time.
- For any non‑2xx status, throw a standardized error object that includes
status,message, and optionally the original payload. - Optionally send the error to a reporting service (e.g., Sentry).
// Continue in src/api/axiosInstance.js
api.interceptors.response.use(response => {
// Log successful responses
console.debug(`[Axios] ${response.config.method.toUpperCase()} ${response.config.url} → ${response.status}`);
return response; // Axios already parsed JSON for us
}, error => {
// Log error details
console.error(`[Axios] Error ${error.config?.method?.toUpperCase()} ${error.config?.url}`, error);
// Build a consistent error shape
const normalizedError = {
status: error.response ? error.response.status : null,
message: error.message,
// Include backend payload if present
details: error.response ? error.response.data : undefined,
};
// Example: forward to an error‑reporting service
// reportError(normalizedError);
return Promise.reject(normalizedError);
});
Verification tip: Add a console.log inside the interceptor (as shown) and open the browser devtools. You should see the log appear before the network request leaves the client.
Worked Example: Fetching a User Profile
With the instance ready, calling an endpoint becomes concise:
// src/services/userService.js
import api from '../api/axiosInstance';
export const getUserProfile = async userId => {
const response = await api.get(`/users/${userId}`);
// response.data is already a parsed JSON object
return response.data;
};
// Usage in a React component
import { getUserProfile } from '../services/userService';
function UserProfile({ userId }) {
const [profile, setProfile] = React.useState(null);
const [error, setError] = React.useState(null);
React.useEffect(() => {
getUserProfile(userId)
.then(data => setProfile(data))
.catch(err => setError(err));
}, [userId]);
if (error) return Error: {error.message};
if (!profile) return Loading…;
return {profile.name};
}
Notice that:
- The
Authorizationheader is added automatically. - No manual
JSON.parseis required; Axios deserializes the response body. - Any network failure or HTTP error arrives as the normalized error object, simplifying
catchhandling.
Trade‑Offs and Limitations
While interceptors centralize concerns, they introduce a few considerations:
- Debugging opacity: When multiple interceptors modify the
configobject, it can be hard to trace why a request ended up with a particular header or payload. Keep each interceptor focused and avoid heavy transformations. - Order matters: Interceptors run in the order they are added. If you need to log the final URL after token injection, ensure the logging interceptor is placed after the token interceptor.
- Cancel token deprecation: Axios’
CancelTokenAPI was removed in v1.x. Use the nativeAbortControllerwith thesignaloption instead of relying on the removed feature. - Global vs. per‑instance configuration: If you need different base URLs or timeouts for separate microservices, create multiple Axios instances rather than trying to override globals mid‑runtime.
Practical Way to Check the Result
After implementing the interceptor:
- Open the browser’s Network tab.
- Trigger a request (e.g., load a page that calls
getUserProfile). - Verify that the
Authorizationheader appears in the request headers. - Check the console for the debug logs you added.
- Force an error (e.g., disconnect the network or point to a non‑existent endpoint) and confirm that the caught error matches the normalized shape you defined.
Actionable Closing
If your application already uses Axios, start by extracting the repetitive auth‑header and logging logic into a request interceptor, then add a response interceptor to shape errors. Keep each interceptor small, test the flow with console logs, and replace any legacy CancelToken usage with AbortController. This approach reduces boilerplate, improves consistency, and gives you a single place to adjust cross‑cutting concerns without touching every API call.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.