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
});
}
}
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.