Chained Promises

AngularJS - Chained PromisesAngularJS Video Tutorial by Thomas Burleson

Promises are a fantastic tool in AngularJS. Many times, as you start to chain them together, they become ugly and unwieldy. In this lesson, Thomas will show you an approach for breaking your chained promises down into a flat, clean, readable structure.


The ability to reply to discussions is limited to PRO subscribers. Want to join in the discussion? Click here to subscribe now.

egghead.io comment guidelines

egghead.io
Avatar

Promises are a fantastic tool in AngularJS. Many times, as you start to chain them together, they become ugly and unwieldy. In this lesson, Thomas will show you an approach for breaking your chained promises down into a flat, clean, readable structure.

Jasper Moelker
Avatar

Thank you for explaining the key concept of chaining promises with a real-life example!
I do want to remark that both travelService.getFlight & weatherService.getForecast use the departure data that is already available when travelService.getDeparture is resolved. Therefore I think getForecast should not be chained to getFlight but rather directly to getDeparture, just like getFlight. I know this doesn't matter much when everything is resolved in local services, but if this would be remote requests it would delay the process longer than needed, which defeats JavaScript's asynchronous power.


FlightServices.js

(function(angular) {
    "use strict";


    angular.module( "FlightServices", [ ] )
        .config( window.$QDecorator )
        .service( "user", function()
        {
            return {
               email      : "ThomasBurleson@Gmail.com",
               repository : "https://github.com/ThomasBurleson/angularjs-FlightDashboard"
            };
        })
        .service( "travelService", function( user, $q )
        {
            // Flight API (each returns a promise)
            return {
                getDeparture : function( user )
                {
                    var dfd = $q.defer();

                        // Mock departure information for the user's flight

                        dfd.resolve({
                            userID   : user.email,
                            flightID : "UA_343223",
                            date     : "01/14/2014 8:00 AM"
                        });

                    return dfd.promise;

                },
                getFlight : function( flightID )
                {
                    return $q.resolve ({
                        id    : flightID,
                        pilot : "Captain Morgan",
                        plane : {
                            make  : "Boeing 747 RC",
                            model : "TA-889"
                        },
                        status: "onTime"
                    });
                }
            };
        })
        .service( "weatherService", function( $q )
        {
            // Weather API (each returns a promise)
            return {
                getForecast : function( date )
                {
                    return $q.resolve({
                        date     : date,
                        forecast : "rain"
                    });
                }
            };

        });


}(window.angular));

Dashboard.js

    var FlightDashboard = function( $scope, user, travelService, weatherService )
        {
            var loadDeparture = function( user )
                {
                    return travelService
                            .getDeparture( user.email )                     // Request #1
                            .then( function( departure )
                            {
                                $scope.departure = departure;               // Response Handler #1
                                return departure.flightID;
                            });
                },
                loadFlight = function( flightID)
                {
                    return travelService
                            .getFlight( flightID )                          // Request #2
                            .then( function( flight )
                            {
                                $scope.flight = flight;                     // Response Handler #2
                                return flight;
                            });
                },
                loadForecast = function()
                {
                    return weatherService
                            .getForecast( $scope.departure.date )           // Reqeust #3
                            .then(function( weather )
                            {
                                $scope.weather = weather;                   // Response Handler #3
                                return weather;
                            });
                };


            // 3-easy steps to load all of our information...

            loadDeparture( user )
                .then( loadFlight )
                .then( loadForecast );


            $scope.user       = user;
            $scope.departure  = null;
            $scope.flight     = null;
            $scope.weather    = null;

        };

$QDecorator.js

(function ( window ){
    "use strict";

        /**
         * Decorate the $q service instance to add extra
         * `spread()` and `resolve()` features
         */
    var $QDecorator = function ($provide)
        {
                // Partial application to build a resolve() function

            var resolveWith = function( $q)
                {
                    return function resolved( val )
                    {
                        var dfd = $q.defer();
                        dfd.resolve( val );

                        return dfd.promise;
                    };
                };

            // Register our $log decorator with AngularJS $provider

            $provide.decorator('$q', ["$delegate",
                function ($delegate)
                {
                    if ( angular.isUndefined( $delegate.spread ))
                    {
                        // Let's add a `spread()` that is very useful
                        // when using $q.all()

                        $delegate.spread = function( targetFn,scope )
                        {
                            return function()
                            {
                                var params = [].concat(arguments[0]);
                                targetFn.apply(scope, params);
                            };
                        };
                    }

                    if ( angular.isUndefined( $delegate.resolve ))
                    {
                        // Similar to $q.reject(), let's add $q.resolve()
                        // to easily make an immediately-resolved promise
                        // ... this is useful for mock promise-returning APIs.

                        $delegate.resolve = resolveWith($delegate);
                    }

                    return $delegate;
                }
            ]);
        };


    if ( window.define != null )
    {
        window.define([ ], function ( )
        {
            return [ "$provide", $QDecorator ];
        });

    } else {

        window.$QDecorator = [ "$provide", $QDecorator ];
    }

})( window );

index.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>AngularJS Promise-Chaining Demo</title>

        <link href="./css/demo.css" rel="stylesheet">
        <script src="./lib/angular.js"      ></script>

        <script src="./lib/$QDecorator.js"  ></script>
        <script src="src/FlightServices.js"></script>
        <script src="src/Dashboard_3.js"></script>

        <script>

            // Start the AngularJS engine for MyFlightApp

            angular.module(
                        "FlightDemo",
                        [ "FlightServices" ]
                    )
                    .controller(
                        "flightDashboard",
                        FlightDashboard
                    );
        </script>

    </head>
    <body ng-app="FlightDemo" ng-controller="flightDashboard" >

        <h1>Hello  <span class="user">{{ user.email }}</span></h1>


        <div class="itinerary">
            <p>Here is your itinerary:</p>

            <span class="title"> Your Departure:   </span> {{ departure.date }}                               <br/>
            <span class="title"> Your Flight:      </span> {{ flight.plane.make }} {{ flight.plane.model }} <br/>
            <span class="title"> Weather Forecast: </span> {{ weather.forecast }}                             <br/>
        </div>

        <footer>
            <a href="https://github.com/ThomasBurleson/angularjs-FlightDashboard">
                GitHub Source
            </a>
        </footer>
    </body>
</html>

Thomas: Promise chains allow us to manage sequences of asynchronous requests and responses. To demonstrate techniques for changing promises, I prepared a test application called The Flight Demo. The flight demo displayed here in the browser will show flight departure, plane information, and a weather forecast.

While the page view is itself visually trivial, the real asynchronous work is done under the hood. I've already prepared the Mock data services where you see here both the travel service and the weather service have API's that return promises.

Here we see that the travel service Get Departure returns a promise that is already resolved with mock data that contains the user's upcoming travel information. Now let's get the flight dashboard controller working so the travel information is shown in the dashboard view.

First we need to load the user's departure information from the travel service. The controller uses the injected instance of the travel service to call Get Departure and load the user's departure information. Since Get Departure returns a promise, we attach a success handler that will be called when a promise is resolved. And that promise is resolved after the asynchronous response occurs.

Our success handler is past the value that was used to resolve the promise. In this case, it is the departure information object which we will then publish to the scope.

Let's refresh the flight demo page in the browser. Now we see the user's departure information. However, the page still does not show the plane information, nor the weather forecast. Notice a dependency here if we look back at the data services. Request to load the plane and weather information use departure information. So to get the flight, we need the flight ID. So this means that we must first load the departure information before we can load the plane and weather information.

Since each of those asynchronous service calls also returns a promise, this means we must build a chain of promises. We can visualize the chain of promises as follows: Get Departure is called and then Get Flight is called, and then Get Forecast is called.

Notice that the Get Flight call needs the flight ID provided in the departure information. And likewise, the Get Forecast call needs the departure date.

To get the flight and weather information for the flight demo page, let's make those service calls and use the return promises to update our scope.

Notice how the success handler for the Get Flight is past the flight object. And the success handler for the get forecast is past the weather object, both of which are published to the scope.

Refreshing our page shows the flight and forecast information is now displayed. But wait. The weather information is not showing. Let's take a look at the console and see that we forgot to tell Angular to inject the weather service. So let's fix that real quick.

Refreshing the page again now shows all the information as expected. This approach works and is an example of deeply nested promise chains. Nesting promise chains quickly become messy, however, if you have lots of logic. Is there another way to build our chain of promises?

What if we viewed each request response as a self-contained process? Then we could chain processes. On the right is the refactored flight dashboard controller. But now we have three intuitively named functions-load departure, load flight, and load forecast, all chained together in a flight chain as we see here in the call.

Each of these functions internally makes a service call, gets a promise, and attaches a success handler to the promise. And each handler publishes something to the scope. But two other very important things are now happening. First, notice that each of the segments-load departure, load flight, and load weather returns a promise.

The important thing to realize here is that instead of returning a data object, we are returning another promise. Returning promises allows us to build chains where each segment is only resolved when the promise at that segment resolves. And that promise could itself represent a sub chain.

While a segment is waiting for its promise to resolve or reject, all the remaining segments in that chain are waiting. And, in fact, those segments have not even been called yet. The async request in subsequent segments are queued and haven't even been called yet.

This is promise chaining. This is very powerful. Second, notice that the internal promise success handler, for example, in travel service, get departure, the success handler is here. Notice that this success handler of each segment returns a value; a value that may be passed as an argument value when invoking the next segment of the promise chain.

So get departure's success handler is returning the flight ID, and that value is passed in when we are making the next segment call to load flight.

And while Load Flight returns the flight object, the next segment, Load Weather, ignores that value because it doesn't need any of the flight information. This is an example of a flattened promise chain. And it is now really easy to understand and to manage.

For even more solutions with promise chaining and information on managed promises and rejections, check out the GetHub repository at https://github.com/ThomasBurleson/angularjs-FlightDashboard.

Tech logo bar

Don't miss out on the latest PRO lessons.

Get Your PRO Subscription Now

Because you like code... not PowerPoint slides.