AngularJS Promises

Chaining, failures, notifications

AngularJS promises are an extremely powerful tool. They allow you to make a multi-layered and complex system based on asynchronous functions, with error and in-progress notification handling, all without getting into callback hell.

This post attempts to explain both creating and using AngularJS promises. It assumes some familiariy with AngularJS, specifically defining and injecting services and using controllers.

What are AngularJS promises?

At the most low-level, a promise is an plain old Javascript object (POJO) with then and finally functions:

{
  'then': function(successCallback, errorCallback, notifyCallback) {
    // Black box code
  },
  'finally': function(finallyCallback) {
    // Black box code
  }
}

and they are returned from functions whose meaningful result will be found or calculated later, i.e. asynchronously. The function will choose which of the callbacks to call, depending on success or failure of the asynchronous action. In case of success, the successCallback is called, and the promise is said to be resolved with a result. In case of failure, the error is called, and the promise is said to have been rejected with an error.

You should note:

  • You won't need to construct a promise yourself using object notation. As will be discussed, they will be created via function calls.
  • Angular promises have other functions, but they just shortcuts, so are ommited for this post.

Using promises

A good first step in understanding promises is to use ones that are created by an existing service. A common use of a promise in AngularJS, are ones created by the $http service.

var promise = $http.get('/my-url');
promise.then(function(result) {
  // Do something with the results of the GET request
});

In the preceeding example, the success of the GET request will cause the $http service to resolve the promise with the result of the GET request. This will then call the successCallback passing this resolved value as the first parameter.

It's quite common to not assign promises to an intermediate variable. The following is equivalent to the preceding example.

$http.get('/my-url').then(function(result) {
  // Do something with the results of the GET request
});

In the above examples, only the successCallback is used from the promise. If we want to handle a failure then

$http.get('/my-url-that-might-fail').then(function(result) {
  // Do something with the result of the GET request if it succeeds
}, function(error) {
  // Do something with the error if it fails
});

Although not currently possible, if $http ever ends up being modified to give in-progress notfications, then this could be used as

$http.get('/my-url').then(function(result) {
  // Do something with the results of the GET request
}, function(error) {
  // Do something with the error
}, function(update) {
  // Do something with the update
});

If you want to perform some action after the $http request has completed, whether it succeeded or failed, then you can use the finally function.

$http.get('/my-url').finally(function() {
  // Do something after either success or failure
});

The success, failure, notify callbacks can all be ommited, or passed null if you have no action to be performed

Chaining Promises

The real power from promises comes from chaining. The first key to understanding this is

The then and finally functions each return a new promise, known as a derived promise.

An example of using derived promises:

var promiseA = $http.get('/my-url');
var promiseB = promiseA.then(function(result) {
  // Do something with the results of the GET request
});
promiseB.then(function(result) {
  // Do something with the resolved result of promiseB
})

As with the case of a single promise, it is quite common to not assign the derived promises to intermediate variables. The following is equivalent to the preceeding example:

$http.get('/my-url').then(function(result) {
  // Do something with the result of the GET request
}).then(function(results) {
  // Do something with the resolved result of promiseB
})

The second key to understanding chains are:

Derived promises are resolved/rejected with the returned resolved/rejected value of the callback that was run.

In practice, this means there are 3 possible ways of controlling the derived promise. It can be resolved immediately, it can be rejected immediately, or its own resolution/rejection can be deferred further until a 3rd promise has been resolved/rejected, in which case the derived promise is resolved/rejected with the 3rd promise's resolved/rejected value.

Resolving a derived promise immediately

If you return anything but a promise from the callback that is run, the derived promise will be resolved immediately with that returned value

$http.get('/my-url').then(function(result) {
  return 'my-immediate-value';
}).then(function(results) {
  // results === 'my-immediate-value';
})

Be aware that not explicty returning a value means that you have returned undefined, and the derived promise will be resolved with undefined.

$http.get('/my-url').then(function(result) {
  // Not returning a value.
}).then(function(results) {
  // results === undefined
})

The above applies to both the success and the error callbacks. Returning any non-promise value from the error callback means that the derived promise will be resolved, and not rejected:

$http.get('/my-url-that-does-not-exist').then(function(results) {
}, function(error) {
  return 'my-immediate-value';
}).then(function(results) {
  // results === 'my-immediate-value'
})

As with the success callback, not returning a value from the error callback means the derived promise will be resolved with undefined

$http.get('/my-url-that-does-not-exist').then(function(result) {
}, function(error) {
  // Not returning a value.
}).then(function(results) {
  // results === undefined
})

I've done the above accidentally: it derives a resolved promise from a rejected one, which might be be desirable.

Rejecting a derived promise immediately

You may want to reject a derived promise, even if the original promise was resolved. This is done by returning the result of $q.reject().

$http.get('/my-url').then(function(result) {
  return $q.reject('my-failure-reason');
}).then(function(results) {
  // The code never gets here
}, function(error) {
  // error === 'my-failure-reason'
});

If you want to then fail the derived promise from an error callback, then you can do the same thing in it:

$http.get('/my-url-that does not exist').then(function(results) {
  // The code never gets here if the GET was unsuccessfull
}, function(error) {
   return $q.reject('my-failure-reason');
}.then(function() {
  // The code never gets here if the GET was unsuccessfull
}, function(error) {
  // error === 'my-failure-reason'
});

Deferring a derived promise

A powerful aspect of derived promises is that their resolution/rejection can be deferred until another promise has been resolved/rejected. This is done by returning a promise from the success or error callback. For example, if you want to run $http.get calls sequentially, and then do something after the final is successful, you can do this by returning the result of $http.get from the callback:

$http.get('/my-first-url').then(function(results) {
  return $http.get('/my-second-url')
}).then(function(results) {
  // results here are the results of the GET to /my-second-url 
});

Because each then call again returns a promise, you can easily add to this chain:

$http.get('/my-first-url').then(function(results) {
  return $http.get('/my-second-url')
}).then(function(results) {
  return $http.get('/my-third-url')
}).then(function(results) {
  // results here are the results of the GET to /my-third-url 
});

Rejection/error handling in promise chains

If a promise is rejected, then every subsequent promise in the chain will be rejected, until one is reached with an error callback.

$http.get('/my-first-url-that-fails').then(function(results) {
  // Never called
  return $http.get('/my-second-url')
}).then(function(results) {
  // Never called
  return $http.get('/my-third-url')
}).then(function(results) {
  // results here are the results of the GET to /my-third-url 
}, function(error) {
  // Error callback called
});

When specifying an error callback, be careful what you return. If you return a non-promise value, which includes undefined by not specifying a return value, then the derived promise for that callback will be resolved, and not rejected.

$http.get('/my-first-url-that-fails').then(function(result) {
  // Never called
  return $http.get('/my-second-url')
}, function(error) {
  // Error callback called
}).then(function(results) {
  // This *is* called, because the previous
  // callback returned undefined
  return $http.get('/my-third-url')
});

Layered APIs

A common use of promises is chaining them via layered APIs. A typical pattern in AngularJS is to have calls to $http functions in a service, so controllers are not aware that $http is used.

MyController -> MyService -> $http

You can do this using a structure like:

// In MyService
this.fetchResults = function() {
  return $http.get('/my-url');
};

// In MyController
$scope.fetchResults = function() {
  MyService.fetchResults().then(function(results) {
    // Do something with results
  });
}

However, this means that the controller will be exposed to HTTP headers and statuses. To hide this lower-level detail, you can add post-processing in the service via a derived promises:

this.fetchResults = function() {
  return $http.get('/my-url').then(function(results) {
    // Just return the HTTP body
    return results.data;
  );
};

You can also include some of your own error handling, so in the case of a failed request, the controller can be ignorant of any details of HTTP:

// In MyService
this.fetchResults = function() {
  return $http.get('/my-url').then(function(results) {
    // Just return the http body
    return results.data;
), function(error) {
  return $q.reject('Oh no!');
});

// In MyController
$scope.fetchResults = function() {
  MyService.fetchResults().then(function(results) {
    // Do something with results
  }, function(error) {
    // Do something with error if it occurs
    // which would be 'Oh no!'
  });
}

Creating Promises

If you're not chaining onto an existing promise, you might need to create a new, non-derived, promise. You can do this by calling $q.defer(). This returns a deferred object:

var deferred = $q.defer();

The deferred object contains a promise, and methods to call to control that promise:

{
  'resolve': function(result) {
    // Black box code
  },
  'reject': function(error) {
    // Black box code
  },
  'notify': function(update) {
    // Black box code
  },
  'promise': // Promise as described above
}

When you want to resolve the promise, you can call the resolve function. Similarly for the reject and notify functions.

Using this, a simple timer (ignoring the existence of $timeout) can be written as

var simpleTimeout = function(time) {
  var deferred = $q.defer();
  $window.setTimeout(function() {
    deferred.resolve('Done!');
  }, time);
  return deferred.promise;
}

Which could be used as:

simpleTimeout(1000).then(function(result) {
  // Code gets here after 1 second, and
  // result === 'Done!'
});

You could also have a timer that sends a notification:

var simpleTimeout = function(time) {
  var deferred = $q.defer();

  $window.setTimeout(function() {
    deferred.resolve('Done!');
  }, time);$

  $window.setTimeout(function() {
    deferred.notify('Half way there!');
  }, time/2);

  return deferred.promise;
}

Which could be use as:

simpleTimeout(1000).then(function(result) {
  // Code gets here after 1 second, and
  // result === 'Done!'
}, null, function(update) {
  // After 1/2 second, code gets here
  // and update === 'Half way there!'
)};

Exceptions thrown in callbacks

A not very documented feature of Angular promises is that when exceptions are thrown in the callbacks, via throw, then the derived promise will be rejected with that thrown value.

$http.get('/my-url').then(function(results) {
  throw 'my-failure-reason';
}).then(function(results) {
  // The code never gets here
}, function(error) {
  // error === 'my-failure-reason'
});

This will also trigger Angular's registered exception handler, so it's not quite equivalent to using $q.reject().