Angular $q promises with timeouts

Extend promises returned by $q with timeout method

I showed how to extend the default Q.promise.timeout method with callback functions in the previous blog post.

In this blog post I will show how to extend promises returned by the AngularJs $q service with timeout method. Because angular uses a lite version of the Q library, the promises returned when you call $q.defer() have no timeout method at all.

Why would you need to have a separate method just to detect promises that never resolved? To simplify debugging and performance reporting mostly. For example, we could report every ajax request that took longer than 10 seconds to Sentry

1
2
3
4
5
6
7
$http(url)
.timeout(10000, function (defer) {
Raven.captureMessage('Request timed out', { url: url });
// Note: the promise has not been rejected
// it is up to you.
})
.then(function onSuccess() {}, function onFailed() {});

We need to modify the promise object returned by $q.defer() call. AngularJs' $providers are the way to do this, as shown in Extending $q promises by @wejendorp.

First, let us create a module that will decorate the $q service.

1
2
3
4
angular.module('ng-q-timeout', [])
.config(['$provide', function ($provide) {
// decorate $q service
});

Second, let us use the .decorate method to change the $q

1
2
3
4
5
6
7
8
9
10
11
12
.config(['$provide', function ($provide) {
$provide.decorator('$q', function decorateQ($delegate) {
// $delegate is the native angular $q service
var _defer = $delegate.defer;
// keep the original $q.defer function

$delegate.defer = function newDefer() {
var d = _defer(); // create the deferred object as before
d.promise.timeout = function (ms, callback) { ... };
return d;
};
});

We have wrapped the original $q.defer inside newDefer function, and added timeout method to the promise object before returning it to the calling code.

All we need to do now is to actually set a timeout and call the user supplied callback.

1
2
3
4
5
6
7
d.promise.timeout = function (ms, callback) {
setTimeout(function () {
if (pending) {
cb(deferred);
}
}, ms);
};

The pending variable keeps track if the promise has been resolved (or rejected), because we do not want to signal the timeout if the promise has already been resolved. Please see the source code how it is set.

The user's callback is then responsible for two actions:

1: Rejecting the promise if desired and handling the rejection inside the catch callback

1
2
3
4
5
6
7
8
9
10
11
.timeout(1000, function (defer) {
logger.error('foo timed out');
defer.reject();
})
.catch(function (err) {
if (!err) {
// probably timed out, do nothing
} else {
logger.error('foo failed with error', err);
}
});

2: We are using native setTimeout function, which is not integrated with the angular's event loop (unlike $timeout but we cannot inject $timeout into .config). If you do any model modifications inside the callback, you need to kick off the $digest cycle by calling $scope.$apply

1
2
3
4
5
6
7
.timeout(1000, function (defer) {
var applier = $scope.$$phase ? function (cb) { cb(); } : $scope.$apply;
applier(function () {
$scope.errorMessage = 'Foo timed out!';
});
defer.reject();
});

Full implementation including tests is at bahmutov/angular-q-timeout and is available on bower as ng-q-timeout.

limitation

  • Each .then function returns a different promise object. We only add the timeout method to the original promise created in $q.defer(), so if you want a time out, it has to be the first call on the promise.