Promises

Angular’s event system (which we discuss in depth in the under the hood chapter) provides a lot of power to our Angular apps. One of the most powerful features it gives us is the automatic resolution of promises.

What’s a Promise?

A promise is a method of resolving a value (or not) in an asynchronous manner. Promises are objects that represent the return value or thrown exception that a function may eventually provide. Promises are incredibly useful in dealing with remote objects, and we can think of them as a proxy for our remote objects.

Traditionally, JavaScript uses closures, or callbacks, to respond with meaningful data that is not available synchronously, such as XHR requests after a page has loaded. Rather than depending upon a callback to fire, we can interact with the data as though it has already returned.

Callbacks have worked for a long time, but the developer suffers when using them. Callbacks provide no consistency and no guaranteed call, they steal code flow when depending upon other callbacks, and they generally make debugging incredibly difficult. At every step of the way, we have to deal with explicitly handling errors.

Instead of firing a function and hoping to get a callback run when executing asynchronous methods, promises offer a different abstraction: They return a promise object.

For example, in traditional callback code, we might have a method that sends a message from one user to one of the user’s friends.

// Sample callback code
User.get(fromId, {
  success: function(err, user) {
    if (err) return {error: err};
    user.friends.find(toId, function(err, friend) {
      if (err) return {error: err};
      user.sendMessage(friend, message, callback);
    });
  },
  failure: function(err) {
    return {error: err}
  }
});

This callback pyramid is already getting out of hand, and we haven’t included any robust error-handling code, either. Additionally, we need to know the order in which the arguments are called from within our callback.

The promised-based version of the previous code might look somewhat closer to:

User.get(fromId)
.then(function(user) {
  return user.friends.find(toId);
}, function(err) {
  // We couldn't find the user
})
.then(function(friend) {
  return user.sendMessage(friend, message);
}, function(err) {
  // The user's friend resulted in an error
})
.then(function(success) {
  // user was sent the message
}, function(err) {
  // An error occurred
});

Not only is this code more readable; it is also much easier to grok. We can guarantee that the callback will resolve to a single value, rather than having to deal with the callback interface.

Notice that in the first example, we have to handle errors differently from how we handle non-errors. We need to make sure when using callbacks to handle errors, we check if an error is defined in the tradition API response signature (usually with (err, data) being the usual method signature). All of our API methods must implement this same structure.

In the second example, we handle the success and error in the same way. Our resultant object will receive the error in the usual manner. The promise API is specific about resolving or rejecting promises, so we also don’t have to worry about our methods implementing a different method signature.

Why Promises?

Escaping from callback hell is just one by-product of using promises. The real point of promises is to make asynchronous functions look more like synchronous ones. With synchronous functions, we can capture both return values and exception values as expected.

We can capture errors at any point of the process and bypass future code that relies upon the error of that process. We achieve all of these things without thinking about the benefits of this synchronous code – it’s simply in the nature of the code.

Thus, the point of using promises is to regain the ability to do functional composition and error bubbling while maintaining the ability of the code to run asynchronously.

Promises are first-class objects and carry with them a few guarantees:

Additionally, we can chain promises and allow the code to process as it would normally run. Exceptions from one promise bubble up through the entire promise chain.

Promises are always executed asynchronously; we can use them without worry that they will block the rest of the app.

Promises in Angular

Angular’s event loop gives Angular the unique ability to resolve promises in its $rootScope.$evalAsync stage (see under the hood for more detail on the run loop). The promises will sit inert until the $digest run loop finishes.

This fact allows for us to turn the results of a promise into the view without any extra work. It also enables us to assign the result of an XHR call directly to a property on a $scope object and think nothing of it.

Let’s build an example that will return a list of open pull requests for AngularJS from GitHub.

Play with it

<h1>Open Pull Requests for Angular JS</h1>
  <ul ng-controller="DashboardController">
  <li ng-repeat="pr in pullRequests">
    {{ pr.title }}
  </li>
</ul>

If we have a service that returns a promise (covered in depth in the services chapter), we can simply interact with the promise in the .then() method which allows us to modify any variable on the scope and place it in the view and expect that Angular will resolve it for us:

angular.module('myApp', [])
.controller('DashboardController', [
  '$scope', 'GithubService', 
    function($scope, GithubService) {
      // GithubService's getPullRequests() method
      // returns a promise
      GithubService.getPullRequests(123)
      .then(function(data) {
        $scope.pullRequests = data.data;
      });
}]);

When the asynchronous call to getPullRequests returns, the $scope.pullRequests value will be available in the .then() method which will then update the $scope.pullRequests array.

How to Create a Promise

In order to create a promise in Angular, we can use the built-in $q service. The $q service provides a few methods in its deferred API.

First, we need to inject the $q service into the object where we want to use it.

angular.module('myApp', [])
.factory('GithubService', ['$q', function($q) {
  // Now we have access to the $q library
}]);

To create a deferred object, we call the method defer():

var deferred = $q.defer();

The deferred object exposes three methods and the single promise property that we can use to deal with the promise.

The resolve function resolves the deferred promise with the value.

deferred.resolve({name: "Ari", username: "@auser"});

This method rejects the deferred promise with a reason. It is equivalent to resolving a promise with a rejection.

deferred.reject("Can't update user");
// Equivalent to 
deferred.resolve($q.reject("Can't update user"));

This method responds with the status of a promises execution.

For example, if we want to return a status from the promise, we can use the notify() function to deliver it.

Let’s say that we have several long-running requests that we want to make from a single promise. We can call the notify function to send back a notification of progress:

.factory('GithubService', function($q, $http) {
  // get events from repo
  var getEventsFromRepo = function() {
    // task
  }
  var service = {
    makeMultipleRequests: function(repos) {
      var d = $q.defer(),
          percentComplete = 0,
          output = [];
      for (var i = 0; i < repos.length; i++) {
        output.push(getEventsFromRepo(repos[i]));
        percentComplete = (i+1)/repos.length * 100;
        d.notify(percentComplete);
      }
        d.resolve(output);

      return d.promise;
    }
  }
  return service;
});

With this makeMultipleRequests() function on our GithubService object, we will receive a progress notification every time a repo has been fetched and processed.

We can use this notification in our usage of the promise by adding a third function call to the promise usage. For instance:

.controller('HomeController', 
function($scope, GithubService) {
  GithubService.makeMultipleRequests([
    'auser/beehive', 'angular/angular.js'
  ])
  .then(function(result) {
    // Handle the result
  }, function(err) {
    // Error occurred
  }, function(percentComplete) {
    $scope.progress = percentComplete;
  });
});
 
This page is a preview of ng-book.
Get the rest of this chapter plus 600 pages of the best Angular content on the web.

 

Ready to master AngularJS?

  • What if you could master the entire framework – with solid foundations – in less time without beating your head against a wall? Imagine how quickly you could work if you knew the best practices and the best tools?
  • Stop wasting your time searching and have everything you need to be productive in one, well-organized place, with complete examples to get your project up without needing to resort to endless hours of research.
  • You will learn what you need to know to work professionally with ng-book: The Complete Book on AngularJS or get your money back.
Get it now