AngularJS : Where to use promises?

ghz 1years ago ⋅ 5406 views

Question

I saw some examples of Facebook Login services that were using promises to access FB Graph API.

Example #1 :

this.api = function(item) {
  var deferred = $q.defer();
  if (item) {
    facebook.FB.api('/' + item, function (result) {
      $rootScope.$apply(function () {
        if (angular.isUndefined(result.error)) {
          deferred.resolve(result);
        } else {
          deferred.reject(result.error);
        }
      });
    });
  }
  return deferred.promise;
}

And services that used "$scope.$digest() // Manual scope evaluation" when got the response

Example #2 :

angular.module('HomePageModule', []).factory('facebookConnect', function() {
    return new function() {
        this.askFacebookForAuthentication = function(fail, success) {
            FB.login(function(response) {
                if (response.authResponse) {
                    FB.api('/me', success);
                } else {
                    fail('User cancelled login or did not fully authorize.');
                }
            });
        }
    }
});

function ConnectCtrl(facebookConnect, $scope, $resource) {

    $scope.user = {}
    $scope.error = null;

    $scope.registerWithFacebook = function() {
        facebookConnect.askFacebookForAuthentication(
        function(reason) { // fail
            $scope.error = reason;
        }, function(user) { // success
            $scope.user = user
            $scope.$digest() // Manual scope evaluation
        });
    }
}

JSFiddle

The questions are:

  • What is the difference in the examples above?
  • What are the reasons and cases to use $q service?
  • And how does it work?

Answer

This is not going to be a complete answer to your question, but hopefully this will help you and others when you try to read the documentation on the $q service. It took me a while to understand it.

Let's set aside AngularJS for a moment and just consider the Facebook API calls. Both the API calls use a callback mechanism to notify the caller when the response from Facebook is available:

  facebook.FB.api('/' + item, function (result) {
    if (result.error) {
      // handle error
    } else {
      // handle success
    }
  });
  // program continues while request is pending
  ...

This is a standard pattern for handling asynchronous operations in JavaScript and other languages.

One big problem with this pattern arises when you need to perform a sequence of asynchronous operations, where each successive operation depends on the result of the previous operation. That's what this code is doing:

  FB.login(function(response) {
      if (response.authResponse) {
          FB.api('/me', success);
      } else {
          fail('User cancelled login or did not fully authorize.');
      }
  });

First it tries to log in, and then only after verifying that the login was successful does it make the request to the Graph API.

Even in this case, which is only chaining together two operations, things start to get messy. The method askFacebookForAuthentication accepts a callback for failure and success, but what happens when FB.login succeeds but FB.api fails? This method always invokes the success callback regardless of the result of the FB.api method.

Now imagine that you're trying to code a robust sequence of three or more asynchronous operations, in a way that properly handles errors at each step and will be legible to anyone else or even to you after a few weeks. Possible, but it's very easy to just keep nesting those callbacks and lose track of errors along the way.

Now, let's set aside the Facebook API for a moment and just consider the Angular Promises API, as implemented by the $q service. The pattern implemented by this service is an attempt to turn asynchronous programming back into something resembling a linear series of simple statements, with the ability to 'throw' an error at any step of the way and handle it at the end, semantically similar to the familiar try/catch block.

Consider this contrived example. Say we have two functions, where the second function consumes the result of the first one:

 var firstFn = function(param) {
    // do something with param
    return 'firstResult';
 };

 var secondFn = function(param) {
    // do something with param
    return 'secondResult';
 };

 secondFn(firstFn()); 

Now imagine that firstFn and secondFn both take a long time to complete, so we want to process this sequence asynchronously. First we create a new deferred object, which represents a chain of operations:

 var deferred = $q.defer();
 var promise = deferred.promise;

The promise property represents the eventual result of the chain. If you log a promise immediately after creating it, you'll see that it is just an empty object ({}). Nothing to see yet, move right along.

So far our promise only represents the starting point in the chain. Now let's add our two operations:

 promise = promise.then(firstFn).then(secondFn);

The then method adds a step to the chain and then returns a new promise representing the eventual result of the extended chain. You can add as many steps as you like.

So far, we have set up our chain of functions, but nothing has actually happened. You get things started by calling deferred.resolve, specifying the initial value you want to pass to the first actual step in the chain:

 deferred.resolve('initial value');

And then...still nothing happens. To ensure that model changes are properly observed, Angular doesn't actually call the first step in the chain until the next time $apply is called:

 deferred.resolve('initial value');
 $rootScope.$apply();

 // or     
 $rootScope.$apply(function() {
    deferred.resolve('initial value');
 });

So what about error handling? So far we have only specified a success handler at each step in the chain. then also accepts an error handler as an optional second argument. Here's another, longer example of a promise chain, this time with error handling:

 var firstFn = function(param) {
    // do something with param
    if (param == 'bad value') {
      return $q.reject('invalid value');
    } else {
      return 'firstResult';
    }
 };

 var secondFn = function(param) {
    // do something with param
    if (param == 'bad value') {
      return $q.reject('invalid value');
    } else {
      return 'secondResult';
    }
 };

 var thirdFn = function(param) {
    // do something with param
    return 'thirdResult';
 };

 var errorFn = function(message) {
   // handle error
 };

 var deferred = $q.defer();
 var promise = deferred.promise.then(firstFn).then(secondFn).then(thirdFn, errorFn);

As you can see in this example, each handler in the chain has the opportunity to divert traffic to the next error handler instead of the next success handler. In most cases you can have a single error handler at the end of the chain, but you can also have intermediate error handlers that attempt recovery.

To quickly return to your examples (and your questions), I'll just say that they represent two different ways to adapt Facebook's callback-oriented API to Angular's way of observing model changes. The first example wraps the API call in a promise, which can be added to a scope and is understood by Angular's templating system. The second takes the more brute-force approach of setting the callback result directly on the scope, and then calling $scope.$digest() to make Angular aware of the change from an external source.

The two examples are not directly comparable, because the first is missing the login step. However, it's generally desirable to encapsulate interactions with external APIs like this in separate services, and deliver the results to controllers as promises. That way you can keep your controllers separate from external concerns, and test them more easily with mock services.