Centralize Auth and Error Handling with Axios Interceptors
A task guide to using Axios interceptors for centralized auth tokens and global error handling, including setup, verification, and recovery.
26 Jan 2026, 05:38 UTC

The problem: the same auth and error logic in every call
When each component attaches its own Authorization header and handles expired sessions separately, small differences appear: one call sends a stale token, another forgets to redirect on 401, and a third logs out on a network error that was not actually an auth failure. The useful takeaway is to move that cross-cutting logic into one Axios instance with request and response interceptors, and to treat the return value of every interceptor as mandatory.
Desired outcome
Create a shared HTTP client that automatically injects a bearer token into outgoing requests and centrally handles authentication failures. The client should be easy to audit, removable without editing call sites, and should not swallow errors that components still need to handle.
Prerequisites and version assumptions
- A JavaScript or TypeScript project with a package manager.
- An API base URL and a token storage location. The example uses
localStorageandimport.meta.env; replace these with your framework equivalents. - Examples assume Axios 1.x. The
axios.create(),interceptors.request.use(),interceptors.response.use(), andeject()APIs are stable in recent 1.x releases, but confirm exact behavior against your installed version.
Install or update Axios from your project root:
npm install axios@^1.7.0
Expected check: npm ls axios reports a 1.x version. Risk: this changes your lockfile; pin an exact version if your release process requires it.
Procedure: build one client, then add two interceptors
1. Create the shared instance
Use axios.create() instead of the global axios default so interceptors do not leak into unrelated HTTP calls.
import axios from 'axios';
export const api = axios.create({
baseURL: import.meta.env.VITE_API_URL,
timeout: 10000,
});
Replace VITE_API_URL with your own environment variable. The timeout is optional but makes hung requests visible during debugging.
2. Add a request interceptor for token injection
A request interceptor receives the config before dispatch. It must return the config; otherwise the request never reaches the network layer.
const authRequestInterceptor = api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('access_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
The second function handles setup failures before the request is sent. Returning Promise.reject(error) keeps the failure visible to the caller.
3. Add a response interceptor for centralized failures
A response interceptor sees successful responses and rejected responses. The success handler should return the response unchanged unless you are transforming data. The error handler should perform global side effects and then re-reject so component-level code can still react.
const authResponseInterceptor = api.interceptors.response.use(
(response) => response,
(error) => {
const status = error.response?.status;
if (status === 401) {
localStorage.removeItem('access_token');
window.location.assign('/login');
}
return Promise.reject(error);
}
);
Use optional chaining on error.response because network errors, timeouts, and cancelled requests may not include a response object. If you handle 500 differently, branch on status before re-rejecting.
4. Keep the ejector functions
Both use() calls return an ejector. Store them if you need to remove the interceptors later, for example in tests or during a rollback.
export function detachAuthInterceptors() {
api.interceptors.request.eject(authRequestInterceptor);
api.interceptors.response.eject(authResponseInterceptor);
}
Expected checks
- Header injection: Open browser DevTools, select an API request made through
api, and confirm theAuthorizationheader contains the expected bearer token. If it is missing, check that the request interceptor returnedconfig. - Unauthorized handling: Temporarily point one endpoint at a mock or local route that returns
401. Call it throughapiand confirm the token is removed and the redirect runs. Restore the endpoint afterward. - Error propagation: In a component, catch the rejected promise and log
error.response?.status. The response interceptor should not convert a failed request into a resolved one. - Config integrity: Add a temporary
console.log(config.url, config.method)in the request interceptor and confirm the original request data is still present after the chain runs.
Recovery options
| Symptom | Likely cause | Recovery |
|---|---|---|
| Request hangs with no network entry | Request interceptor did not return config | Return the config or a rejected promise |
Caller sees undefined after success | Response success handler did not return response | Return the response object |
| 401 handler never runs | Error handler returned a value instead of rejecting, or the failure had no response | Re-reject the error and check for network failures separately |
| Unexpected behavior after a change | Multiple interceptors with hidden ordering or side effects | Eject interceptors, then re-add them one at a time |
Because installing and registering interceptors changes runtime behavior, a practical rollback is to call detachAuthInterceptors() and redeploy the previous client configuration. If the change is already bundled, ejecting at runtime stops the new behavior without editing every call site.
Limitations
- Interceptors are global to the Axios instance. A change affects every request made through that instance, including requests you did not anticipate.
- Do not place complex business rules in interceptors. They are middleware for cross-cutting concerns such as auth, tracing, and generic error logging.
- Async work inside a request interceptor delays the request. Keep token reads synchronous or cache the token before dispatch.
- Browser-only APIs such as
window.locationandlocalStorageneed guards for server-side rendering or Node scripts. - If you register more than one interceptor, do not assume an execution order unless you have verified it for your Axios version; ordering is a common source of subtle bugs.
A final practical check is to run one authenticated request and one forced 401 request after any refactor. If the header appears and the global handler fires while component-level catch blocks still receive the error, the interceptor chain is working as intended.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.