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 aPromise
as it’s only argument. - It’s crucial to note that our
try
method resolves and returns thePromise
(conveniently namedpromise
). This will allow us to chain on the returnedPromise
from 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
Promise
returned by the$http
service in it’s own variable, and passed it as an argument to thetry
method on our retry service. - Since
try
simply tries resolving thepromise
and returns it, we can chain on the returnedpromise
and 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
$http
and$q
services. - We added a
numRetries
argument to ourtry
method which indicates the number of retries we should attempt before giving up on the request. In our error callback, we create a newconfig
object by extending theconfig
property on the returnedresponse
. We also keep track of the number of retries we have attempted by adding a property to ourconfig
object calledretryCount
and setting it to 0. - We created a private
retry
function which takes aconfig
object andnumRetries
as arguments. This is where all of our retry logic lives.retry
is similar totry
since it also returns aPromise
(note that ourtry
method returns_retry
). We use the$http
to 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 theretryCount
property set onconfig
(from inside thetry
method), and then either attempt to retry the request or reject thePromise
depending on thenumRetries
we 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.