I recently found myself in a situation where I had to retry sending a failed HTTP request an arbitrary number of times before returning an error. This particular project was built using Angular, so I decided to create a simple service to solve this problem. By doing so I could avoid getting caught in a nightmare of callbacks, and stay true to the DRY principle; since I could reuse this service in other controllers and services.
Setting up our service
Let’s start by creating a basic skeleton for our new service:
(function() {
'use strict';
angular.module('myAwesomeApp').factory('HttpRetryService', HttpRetryService);
function HttpRetryService() {
var httpRetryService = {};
return httpRetryService;
}
})();
Pretty exciting stuff right? For now, our service does nothing but return an empty Object. Before we can go ahead and change that, we need to think about how we want our service to be used.
Lets assume we have a basic controller called TodoController that has a single method to list all of our current to-dos:
(function() {
'use strict';
angular.module('myAwesomeApp').controller('TodoController', TodoController);
function TodoController($http) {
vm.list = list;
return vm;
/**
* Returns a list of to-dos
*/
function list() {
$http({
method: 'GET',
url: 'my/awesome/api/todo/list'
}).then(
function(response) {
// We got our todos!
vm.todos = response.data;
},
function(response) {
// TODO: Oops! Something went wrong! Let's retry before informing the user!
}
);
}
}
})();
Angular’s $http service and Promises
As you can see, our list method uses the $http service to make a GET request to our /todo/list endpoint. If the request is successful, it sets the returned list of todos on the TodoController. If it fails, we want to retry fetching the todos again before informing the user that something went wrong. So, how do we do that?
Well, we could simply retry sending the request directly in the error callback function. This is definitely our quickest option, but isn’t viable unless we are only going to attempt retrying this one particular request. Otherwise we would have to duplicate this logic whenever we want to do the same thing elsewhere in our code. That’s not good. We should keep things as tight, clean and focused as possible. This is where our new HttpRetryService comes into play.
According to the Angular documentation, “The $http service is a function which takes a single argument — a configuration object — that is used to generate an HTTP request and returns a promise”. Instead of handling the Promise returned by the $http service in our controller’s list method, lets have our HttpRetryService do the work for us:
(function() {
'use strict';
angular
.module('myAwesomeApp')
.factory('HttpRetryService', HttpRetryService);
function HttpRetryService() {
var httpRetryService = {
try: try
};
return httpRetryService;
/**
* Attempts to resolve a Promise, and retries if it fails
* @param promise {Promise}
* @return {Promise}
*/
function try(promise) {
return promise
.then(function(response) {
return response;
}, function(response) {
// TODO: Oops! Something went wrong. Let's retry!
});
}
}
})();
Okay, so what’s going on here? Lets break it down:
- For now, our service only needs one public method,
try, which will accept aPromiseas it’s only argument. - It’s crucial to note that our
trymethod resolves and returns thePromise(conveniently namedpromise). This will allow us to chain on the returnedPromisefrom our Todo controller, and execute any controller-specific logic on the returnedresponse.
Now all we have left to do is our retry logic. Before we jump into that, let’s update our TodoController to use our service in it’s current state.
(function() {
'use strict';
angular.module('myAwesomeApp').controller('TodoController', TodoController);
function TodoController($http, $window, HttpRetryService) {
vm.list = list;
return vm;
/**
* Returns a list of to-dos
*/
function list() {
var promise = $http({
method: 'GET',
url: 'my/awesome/api/todo/list'
});
HttpRetryService.try(promise).then(
function(response) {
vm.todos = response.data;
},
function(response) {
// We tried and re-tried, there's no hope. Let's inform the user.
$window.alert('There was an error! ' + response.data.message);
}
);
}
}
})();
You’ll notice a few changes:
- We injected our new
HttpRetryService. - We stored the
Promisereturned by the$httpservice in it’s own variable, and passed it as an argument to thetrymethod on our retry service. - Since
trysimply tries resolving thepromiseand returns it, we can chain on the returnedpromiseand trigger our controller-specific logic. In this case, we either set the list of todos on our controller, or alert the user that something went wrong.
Setting up our retry logic
The last and most important step is our retry logic in our service. As specified in the Angular docs, the response object returned by the $http service has a property called config, which is the configuration object that was used to generate the original request. We can use config to retry sending the same request an arbitrary number of times before giving up. Let’s modify our service to do just that.
(function() {
'use strict';
angular
.module('myAwesomeApp')
.factory('HttpRetryService', HttpRetryService);
function HttpRetryService($http, $q) {
var httpRetryService = {
try: try
};
return httpRetryService;
/**
* Attempts to resolve a Promise, and retries if it fails
*
* @param promise {Promise}
* @param numRetries (Int)
* @return {Promise}
*/
function try(promise, numRetries) {
return promise
.then(function(response) {
return response;
}, function(response) {
var config = angular.extend({
retryCount: 0
}, response.config);
return _retry(config, numRetries || 3);
});
}
/**
* Retries sending a failed request
*
* @param config {Object}
* @param numRetries {Int}
* @return {Promise}
*/
function _retry(config, numRetries) {
return $http(config)
.then(function(response) {
return response;
}, function(response) {
config.retryCount++;
if (config.retryCount <= numRetries) {
return _retry(config, numRetries || 3);
} else {
return $q.reject(response);
}
});
}
}
})();
We made quite a few modifications to our service, so let’s go through them step by step:
- We injected the
$httpand$qservices. - We added a
numRetriesargument to ourtrymethod which indicates the number of retries we should attempt before giving up on the request. In our error callback, we create a newconfigobject by extending theconfigproperty on the returnedresponse. We also keep track of the number of retries we have attempted by adding a property to ourconfigobject calledretryCountand setting it to 0. - We created a private
retryfunction which takes aconfigobject andnumRetriesas arguments. This is where all of our retry logic lives.retryis similar totrysince it also returns aPromise(note that ourtrymethod returns_retry). We use the$httpto make a request usingconfig(the same configuration object from our original request). If the request is successful, we return thePromise. If the request fails, we increment theretryCountproperty set onconfig(from inside thetrymethod), and then either attempt to retry the request or reject thePromisedepending on thenumRetrieswe are allowed.
Other then that, the logic in our TodoController does not change since we default the numRetries to 3 if it isn’t passed.
And there you have it - a clean, simple and easy to use retry service in Angular.