When making API calls in AngularJS, network failures or temporary server errors (like 500 Internal Server Error
or 503 Service Unavailable
) can cause intermittent failures. Instead of immediately showing an error to the user, we can retry the request automatically a few times before giving up.
In this guide, we’ll implement automatic retry logic using $httpInterceptor
and $q
in AngularJS.
1. Why Implement Retry Logic?
Handles temporary network issues (e.g., weak internet connection)
Improves user experience by reducing error messages
Automatically retries API calls without user intervention
Prevents unnecessary UI disruptions due to transient errors
2. Basic Structure of an HTTP Interceptor
AngularJS $httpInterceptor
allows us to intercept failed HTTP requests and retry them automatically.
Interceptor Lifecycle Hooks
request(config)
→ Modify request before sendingresponse(response)
→ Modify successful responseresponseError(rejection)
→ Handle failed API calls
We’ll use the responseError
method to retry failed requests.
3. Implementing Retry Logic in an $httpInterceptor
Step 1: Create the Interceptor with Retry Logic
angular.module('myApp')
.factory('retryInterceptor', function($q, $injector, $timeout) {
let maxRetries = 3; // Maximum retry attempts
let retryDelay = 2000; // Delay between retries (in milliseconds)
return {
responseError: function(rejection) {
let $http = $injector.get('$http'); // Get $http dynamically
let config = rejection.config; // Original request config
if (!config) return $q.reject(rejection); // Ignore if no config
// Track retry attempts (initialize if undefined)
config.retryCount = config.retryCount || 0;
// Check if the error is temporary (server issues, timeout, etc.)
let shouldRetry = [500, 503, 408].includes(rejection.status);
if (shouldRetry && config.retryCount < maxRetries) {
config.retryCount++; // Increment retry count
return $timeout(() => $http(config), retryDelay); // Retry after delay
}
return $q.reject(rejection); // Reject if max retries reached
}
};
});
Step 2: Register the Interceptor in $httpProvider
After defining the interceptor, we register it globally so that it applies to all API requests.
angular.module('myApp')
.config(function($httpProvider) {
$httpProvider.interceptors.push('retryInterceptor');
});
Automatically retries failed requests up to maxRetries
times
Uses $timeout()
to introduce a delay before retrying
Prevents infinite retries by tracking retryCount
4. Adding Exponential Backoff to Reduce Load
Instead of using a fixed retry delay, we can implement exponential backoff, where the delay doubles after each retry. This helps prevent overloading the server.
Modify the Interceptor to Use Exponential Backoff
.factory('retryInterceptor', function($q, $injector, $timeout) {
let maxRetries = 3;
return {
responseError: function(rejection) {
let $http = $injector.get('$http');
let config = rejection.config;
if (!config) return $q.reject(rejection);
config.retryCount = config.retryCount || 0;
let shouldRetry = [500, 503, 408].includes(rejection.status);
if (shouldRetry && config.retryCount < maxRetries) {
config.retryCount++;
let delay = Math.pow(2, config.retryCount) * 1000; // 2^n * 1000ms
return $timeout(() => $http(config), delay);
}
return $q.reject(rejection);
}
};
});
Retries after increasing delays (e.g., 2s → 4s → 8s
)
Reduces server load by waiting longer after each retry
5. Adding a Global Loader While Retrying
To inform the user that the request is being retried, we can show a loading indicator using $rootScope
.
Step 1: Broadcast Retry Events from the Interceptor
Modify the interceptor to emit events when a retry occurs.
.factory('retryInterceptor', function($q, $injector, $timeout, $rootScope) {
let maxRetries = 3;
return {
responseError: function(rejection) {
let $http = $injector.get('$http');
let config = rejection.config;
if (!config) return $q.reject(rejection);
config.retryCount = config.retryCount || 0;
let shouldRetry = [500, 503, 408].includes(rejection.status);
if (shouldRetry && config.retryCount < maxRetries) {
config.retryCount++;
let delay = Math.pow(2, config.retryCount) * 1000;
$rootScope.$broadcast('apiRetry', config.retryCount); // Notify UI
return $timeout(() => $http(config), delay);
}
return $q.reject(rejection);
}
};
});
Step 2: Listen for Retry Events in a Controller
angular.module('myApp')
.controller('MainController', function($scope) {
$scope.retryCount = 0;
$scope.$on('apiRetry', function(event, count) {
$scope.retryCount = count;
});
});
Step 3: Show a Loading Indicator in the UI
<div ng-if="retryCount > 0">
<p>Retrying API call... Attempt {{ retryCount }}</p>
</div>
Users are informed about retries dynamically
6. Skipping Retry for Specific Requests
Some API requests (like user login) should not be retried. We can mark such requests with a custom flag in the request config.
Step 1: Add a noRetry
Flag in the Request
$http.get('/api/user/login', { noRetry: true }) // This request won't retry
.then(response => console.log(response))
.catch(error => console.error(error));
Step 2: Modify the Interceptor to Skip noRetry
Requests
.factory('retryInterceptor', function($q, $injector, $timeout) {
let maxRetries = 3;
return {
responseError: function(rejection) {
let $http = $injector.get('$http');
let config = rejection.config;
if (!config || config.noRetry) return $q.reject(rejection); // Skip retries
config.retryCount = config.retryCount || 0;
let shouldRetry = [500, 503, 408].includes(rejection.status);
if (shouldRetry && config.retryCount < maxRetries) {
config.retryCount++;
let delay = Math.pow(2, config.retryCount) * 1000;
return $timeout(() => $http(config), delay);
}
return $q.reject(rejection);
}
};
});
Allows skipping retries for specific API calls