Network hiccups causing failed API requests? Axios interceptors can automatically retry failed requests before showing errors to users.
Install Axios:
npm install axios
The Basic Retry Interceptor:
import axios from 'axios';
// Configure retry
axios.interceptors.response.use(
response => response, // Success - return as is
async error => {
const config = error.config;
// If no config or already retried max times, reject
if (!config || !config.retry) {
return Promise.reject(error);
}
// Set retry count
config.__retryCount = config.__retryCount || 0;
// Check if we've maxed out retries
if (config.__retryCount >= config.retry) {
return Promise.reject(error);
}
// Increase retry count
config.__retryCount += 1;
// Delay before retry
const delay = config.retryDelay || 1000;
await new Promise(resolve => setTimeout(resolve, delay));
// Retry request
return axios(config);
}
);
// Usage: Add retry config to requests
axios.get('/api/data', {
retry: 3, // Retry 3 times
retryDelay: 1000 // Wait 1 second between retries
})
.then(response => console.log(response.data))
.catch(error => console.error('Failed after 3 retries'));
Exponential Backoff:
axios.interceptors.response.use(
response => response,
async error => {
const config = error.config;
if (!config || !config.retry) {
return Promise.reject(error);
}
config.__retryCount = config.__retryCount || 0;
if (config.__retryCount >= config.retry) {
return Promise.reject(error);
}
config.__retryCount += 1;
// Exponential backoff: 1s, 2s, 4s, 8s...
const delay = Math.pow(2, config.__retryCount) * 1000;
console.log(`Retry ${config.__retryCount}/${config.retry} after ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
return axios(config);
}
);
// First retry: 2 seconds
// Second retry: 4 seconds
// Third retry: 8 seconds
Retry Only on Specific Status Codes:
axios.interceptors.response.use(
response => response,
async error => {
const config = error.config;
const status = error.response?.status;
// Only retry on network errors or 5xx server errors
const shouldRetry = !status || status >= 500;
if (!shouldRetry || !config?.retry) {
return Promise.reject(error);
}
// Don't retry client errors (4xx)
if (status >= 400 && status < 500) {
console.log('Client error, not retrying:', status);
return Promise.reject(error);
}
config.__retryCount = config.__retryCount || 0;
if (config.__retryCount >= config.retry) {
return Promise.reject(error);
}
config.__retryCount += 1;
const delay = config.retryDelay || 1000;
await new Promise(resolve => setTimeout(resolve, delay));
return axios(config);
}
);
// Retries on:
// - Network failures (no status)
// - 500, 502, 503, 504 (server errors)
// Does NOT retry on:
// - 400 Bad Request
// - 401 Unauthorized
// - 404 Not Found
// (These won't fix themselves by retrying)
Global Retry Configuration:
// Set defaults for all requests
axios.defaults.retry = 3;
axios.defaults.retryDelay = 1000;
// Now all requests auto-retry
axios.get('/api/users'); // Auto-retries 3 times
axios.post('/api/orders'); // Auto-retries 3 times
// Override per request
axios.get('/api/critical', {
retry: 5, // Retry more for critical endpoints
retryDelay: 2000
});
Show Retry Progress to User:
axios.interceptors.response.use(
response => response,
async error => {
const config = error.config;
if (!config?.retry) {
return Promise.reject(error);
}
config.__retryCount = config.__retryCount || 0;
if (config.__retryCount >= config.retry) {
showToast('Request failed after multiple attempts', 'error');
return Promise.reject(error);
}
config.__retryCount += 1;
// Show retry notification
showToast(
`Connection issue. Retrying (${config.__retryCount}/${config.retry})...`,
'warning'
);
const delay = Math.pow(2, config.__retryCount) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
return axios(config);
}
);
// User sees:
// "Connection issue. Retrying (1/3)..."
// "Connection issue. Retrying (2/3)..."
// Either: "Success!" or "Request failed after multiple attempts"
Retry with Authentication Refresh:
let isRefreshing = false;
let failedQueue = [];
axios.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;
// If 401 and hasn't been retried yet
if (error.response?.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
// Queue this request while token refreshes
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
})
.then(token => {
originalRequest.headers.Authorization = `Bearer ${token}`;
return axios(originalRequest);
});
}
originalRequest._retry = true;
isRefreshing = true;
try {
// Refresh token
const { data } = await axios.post('/api/refresh-token');
const newToken = data.access_token;
// Update token
axios.defaults.headers.common.Authorization = `Bearer ${newToken}`;
// Retry all queued requests
failedQueue.forEach(promise => {
promise.resolve(newToken);
});
failedQueue = [];
// Retry original request
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return axios(originalRequest);
} catch (refreshError) {
// Refresh failed - logout user
failedQueue.forEach(promise => {
promise.reject(refreshError);
});
failedQueue = [];
// Redirect to login
window.location.href = '/login';
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
return Promise.reject(error);
}
);
// User stays logged in even if token expires mid-session!
Cancel Retry on User Action:
const CancelToken = axios.CancelToken;
let cancel;
const request = axios.get('/api/slow-endpoint', {
retry: 5,
retryDelay: 2000,
cancelToken: new CancelToken(c => cancel = c)
});
// User clicks "Cancel" button
document.getElementById('cancel-btn').addEventListener('click', () => {
cancel('User cancelled request');
});
