Axios Interceptors: Centralizing Auth, Error Handling, and Payload Normalization
Axios interceptors let you inject auth tokens, handle errors, and normalize payloads globally. This blog walks through a practical example of token refresh, discusses pitfalls, and offers a checklist to keep your interceptor chain clean and predictable.
15 Sept 2025, 18:16 UTC

Why Interceptors Matter
When you build a front‑end that talks to a REST API, you often need to add the same header, handle the same error codes, or massage the data shape for every request. Duplicating that logic across dozens of service functions is error‑prone and hard to maintain. Axios’ interceptor system lets you write that logic once, register it on an axios instance, and have it run automatically for every request or response.
Problem Statement
Developers frequently scatter cross‑cutting concerns—auth tokens, retry logic, logging—inside each API call. This leads to:
- Code duplication and inconsistent behavior
- Hard‑to‑track bugs when the same header is missing in one place
- Difficulty adding new concerns without touching all service modules
We need a single place to manage these concerns and a predictable execution order.
Thesis
Axios interceptors provide a lightweight, promise‑based pipeline that runs before a request is dispatched or after a response is received. By leveraging them, you can:
- Inject auth tokens and refresh them when needed
- Centralize error handling and retry logic
- Normalize API payloads into application‑specific shapes
- Add cross‑cutting logging or metrics without scattering code
How Interceptors Work
Axios exposes two interceptor stacks: request and response. Each stack is a FIFO queue of functions that receive the request config or response object and must return the same object (or a promise that resolves to it). If an interceptor throws or rejects, the chain stops and the error propagates to the caller.
Typical usage:
const instance = axios.create();
instance.interceptors.request.use(onRequest, onRequestError);
instance.interceptors.response.use(onResponse, onResponseError);
Request Interceptors
Run before the HTTP call is made. They can modify the config object, add headers, or even cancel the request by throwing.
Response Interceptors
Run after the response arrives (or an error occurs). They can transform the response.data, trigger side effects, or re‑throw a custom error.
Concrete Example: Auth Token Injection & Refresh
Below is a minimal, reproducible pattern that demonstrates:
- Adding an
Authorizationheader to every request - Refreshing the token if a
401is returned - Retrying the original request once the token is refreshed
Assumptions:
- Token is stored in
localStorageunderaccess_token - Refresh endpoint is
/auth/refreshand returns a new token - All requests are made through the same
axiosInstance
// axiosInstance.js
import axios from 'axios';
const axiosInstance = axios.create({
baseURL: 'https://api.example.com',
timeout: 5000,
});
// Helper to get the current token
const getToken = () => localStorage.getItem('access_token');
// Helper to set a new token
const setToken = (token) => localStorage.setItem('access_token', token);
// Request interceptor: add the token header
axiosInstance.interceptors.request.use((config) => {
const token = getToken();
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
}, (error) => Promise.reject(error));
// Flag to prevent infinite refresh loops
let isRefreshing = false;
let failedQueue = [];
const processQueue = (error, token = null) => {
failedQueue.forEach((prom) => {
if (error) {
prom.reject(error);
} else {
prom.resolve(token);
}
});
failedQueue = [];
};
// Response interceptor: handle 401 and retry
axiosInstance.interceptors.response.use((response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response && error.response.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
// Queue the request until the token refresh finishes
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
})
.then((token) => {
originalRequest.headers['Authorization'] = `Bearer ${token}`;
return axiosInstance(originalRequest);
})
.catch((err) => Promise.reject(err));
}
originalRequest._retry = true;
isRefreshing = true;
try {
const { data } = await axiosInstance.post('/auth/refresh');
const newToken = data.access_token;
setToken(newToken);
axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${newToken}`;
processQueue(null, newToken);
originalRequest.headers['Authorization'] = `Bearer ${newToken}`;
return axiosInstance(originalRequest);
} catch (refreshError) {
processQueue(refreshError, null);
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
return Promise.reject(error);
});
export default axiosInstance;
**How to verify**:
- Use a local mock server that returns
401for the first call and200after the refresh. - Inspect the network tab: the first request should have the old token, the second should have the new one.
- In a Jest test, mock
/auth/refreshwithnockand assert that the original request is retried once.
Trade‑Offs and Common Pitfalls
- Order matters: Interceptors are executed in the order they are added. A later request interceptor sees the output of earlier ones, which can lead to subtle bugs if you rely on a header added by a previous interceptor.
- Duplicate registrations: Importing a module that registers interceptors in multiple places will stack them. Use a singleton
axiosInstanceor eject interceptors when a module is re‑imported. - Synchronous errors: Throwing inside an interceptor will swallow the original error. Always return a rejected promise or re‑throw the error after logging.
- Memory leaks: Long‑running applications with dynamic interceptor registration should eject unused interceptors to keep memory usage bounded.
Actionable Checklist
- Create a dedicated
axiosInstancefor your app and avoid mixing it with the globalaxiosobject. - Register request interceptors first (auth, base headers).
- Register response interceptors last (error handling, data normalization).
- Use
instance.interceptors..eject(id)to remove interceptors when a module is unloaded. - Write unit tests with
nockor similar to mock responses and assert interceptor behavior. - Document the interceptor chain and its expected order so new developers understand the flow.
Conclusion
Axios interceptors give you a clean, promise‑based way to centralize cross‑cutting concerns. When used responsibly—mindful of order, duplication, and error propagation—they can dramatically reduce boilerplate, improve consistency, and make your API layer easier to reason about.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.