This chapter covers
7.3.3 Processing results with promises
A promise is an object that represents the outcome of a process that hasn’t yet com- pleted. When an MV* framework supports promises, its functions that perform asyn- chronous server calls will return a promise that serves as a proxy for the call’s eventual
Listing 7.4 Processing a shopping cart update via callbacks
New Cart instance created
No model attributes to change before saving (null)
59
Using MV* frameworks
results. It’s through this promise that you can orchestrate complex result-handling routines. To understand how to use a promise, you must first understand its internal state before and after the call is made.
WORKINGWITHPROMISESTATES
The good news about working with promises is that they exist in only one of the follow- ing three states:
Fulfilled —This is the state of the promise when the process resolves successfully. The value contained within the promise is the result of the process that ran. In your shopping cart update, this would be the updated cart contents returned by the server.
Rejected —This is the promise’s state when the process fails. The promise con- tains a reason for the failure (usually an Error object).
Pending —This is the initial state of the promise before the process completes. In this state, the promise is neither fulfilled nor rejected.
These three states are mutually exclusive and final. After the promise has been ful- filled or rejected, it’s considered settled and can’t be converted into any other state. Figure 7.5 uses the shopping cart project to illustrate the three states of a promise.
A variable assigned to a promise doesn’t remain a null reference while it waits for the function to return. Instead, a full-fledged object gets returned immediately in a pending state with an undetermined value. When the process finishes, the promise’s
1. The state of the promise begins as pending.
2. When an error occurs, the state is rejected.
3. If the call is successful, the state is fulfilled. Pending Call is successful? Yes ? Rejected Reason Fulfilled Cart data No A fulfilled promise’s value contains the expected results.
If rejected, the promise’s value is the reason for the rejection.
60 CHAPTER 7 Communicating with the server
state changes to either fulfilled, with its value containing the results of the call, or rejected, with the reason for the failure.
ACCESSINGTHERESULTSOFYOURPROCESS
I haven’t talked about what you do with a promise after it’s returned, in order to access a process’s results. The Promise API has several useful methods, but the one you’ll use the most is its then() method.
The then() method lets you register callback functions that allow the promise to hand you back a process’s results. The functions you define here are called reactions. The first reaction function represents the case in which the promise is fulfilled. The second is optional and represents the case in which the promise is rejected:
promise.then(
function (value) {
// reaction to process the success value },
function (reason) {
// reaction to optionally deal with the rejection reason }
);
Because the rejected reaction is optional, the then() method can be written in short- hand:
promise.then(function (value) {
// process the success value, ignore rejection });
Here’s the point to remember about reaction functions: no matter how the code is formatted, only one of the two functions will ever be executed—never both. It’s one or the other. In this regard, it’s somewhat analogous to a try/catch block. It’s also worth noting that the parameter of the reaction function is what the promise hands you back (with either the fulfilled value or the rejection reason). When that happens, you have your results.
Let’s take a look at the then() function in action. The following listing updates your shopping cart and uses a promise instead of a callback function to process the results.
CartDataSrc.updateCart(cartObj).$promise .then(
function(updatedCart) { console.log("Cart ID: " + updatedCart.id); },
function(errorMsg) {
console.log("Error: " + errorMsg); }
);
Listing 7.5 Processing a shopping cart update via a promise
The update returns a promise
Use the promise’s then() function to access the results
61
Using MV* frameworks
Having a promise returned is built into AngularJS’s $resource methods. As you can see in the example, you’re writing out the results of the call to the console as you did before—only this time you’re able to use the returned promise object instead of diverting control over to a callback function. The then() method passes the success results or the rejected reason to the functions you give it.
Another perk of using promises is that you can chain multiple then() methods together if more than one thing needs to happen after your call has been made.
CHAININGPROMISES
Often after a process has run, you want several things to happen after the fact. In addi- tion, you may need these things to happen in order, ensuring that the next event hap- pens only if the one before it succeeds. This is not only possible but also easy to do with promises.
NOTE jQuery’s implementation of promises doesn’t support every scenario described in this section. See https://blog.domenic.me/youre-missing-the- point-of-promises for more details.
So far in your shopping cart update, you’ve been printing the results to the console. In a real application, you want to perform the following tasks after the server call finishes:
1 Recalculate the cart’s total, applying necessary discounts. 2 Update the view with the results.
3 Reuse the message service to update the user that the call was a success.
Moreover, you want these tasks performed in order, and only if each task is successful should the next one begin. This ensures that the user won’t be erroneously notified that everything went swimmingly if an error happens to occur along the way (see the following listing).
var promise = CartDataSrc.updateCart(cartObj).$promise promise.then(function( updatedCart ) { return shoppingCartSvc .calculateTotalCartCosts(updatedCart); }) .then(function(recalculatedCart) { replaceCartInView(recalculatedCart); }) .then(function() { messageSvc.displayMsg("Cart updated!"); }) ["catch"](function(errorResult) { messageSvc.displayError(errorResult); });
Listing 7.6 Using promises to force control flow
$resourc e returns a promise
Return recalculated cart for use in next then()
Display recalculated cart
Display a user message Handle any errors that occurred along the way
62 CHAPTER 7 Communicating with the server
This works because each then() returns a promise. If the reaction of the previous then() returns a promise, its value is used in the subsequent promise handed to the next then(). If the reaction returns a simple value, this value becomes the value in the promise passed forward. This allows you to chain them all together and makes for a straightforward and clean approach.
Being able to chain together multiple tasks in sequence in a few lines of code is amazing, but chaining can help you in other ways. Another amazing thing about chain- ing promises is that you can have more than one asynchronous process in the chain.
CHAININGMULTIPLEASYNCHRONOUSPROCESSESINSEQUENCE
Sometimes when you need several tasks to run in order, more than one may be asyn- chronous. Because you don’t know when asynchronous processes will finish, trying to place one into a sequence with other tasks might be pretty challenging. It’s easy, though, using promises. Because each then() is resolved before the next one is exe- cuted, the entire chain executes sequentially. This is still true even if multiple asyn- chronous processes are in the chain.
To demonstrate, let’s pretend that the server APIs require you to use the cart ID
that’s returned by the shopping cart update in a subsequent GET call in order to prop- erly display the cart onscreen. The following listing illustrates how to use promises to do this.
var promise = CartDataSrc.updateCart(cartObj).$promise promise.then(function( cartReturned ) { return shoppingCartSvc .getCartById(cartReturned.cartId); }) .then(function(fetchedCart) { return shoppingCartSvc .calculateTotalCartCosts(fetchedCart); }) .then(function(recalculatedCart) { replaceCartInView(recalculatedCart); }) .then(function() { messageSvc.displayMsg(userMsg); }) ["catch"](function(errorResult) { messageSvc.displayError(errorResult); });
In this chain, your update happens first. Then, after it returns, your next server call fires. Because the GET call from $resource already creates a promise, its value will be used in the promise passed to the next then().
Before finishing this discussion of promises, let’s get a quick overview of error han- dling. You saw error handling in some of the examples, but I didn’t go over any details.
Listing 7.7 Executing more than one server call in order
Use the cart returned in the update for the next server call
Use the fetched cart for the recalculation
63
Using MV* frameworks