Implementing retry logic for failed API calls

Loading

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 sending
  • response(response) → Modify successful response
  • responseError(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

Leave a Reply

Your email address will not be published. Required fields are marked *