Home » Angularjs » Promise API – combining results of 2 asynchronous call

Promise API – combining results of 2 asynchronous call

Posted by: admin November 30, 2017 Leave a comment

Questions:

With promise API, how to send two asynchronous request in parallel, and resolve the combined result as the response.

var get = function(id){
            var res1, res2;
            var deferred = $q.defer();
            Db.get(id, "abc")
                .then(function (d) {
                    //deferred.resolve(d));
                    res1 = d;
                }, function (e) {
                    //error
                });

            Db.get(id, "def")
                .then(function (d) {
                    //deferred.resolve(d));
                    res2 = d;
                }, function (e) {
                    //error
                });

            //?????? how to return {res1:res1 , res2: res2}

            return deferred.promise;
        };

now, when I call get() like

get(123).then(function(d)){
// d= {res1: res1, res2: res2}
},
...

I need to get the combined result as indicated. How to do this with Angular promise API?

Answers:

As @Matt said, you need to use $q.all, but the usage isn’t quite right. AngularJS doesn’t support .done and .fail and they don’t work quite like that anyway in that there’s no such thing as a promise for multiple values, instead you just have a promise for an array.

If you were writing this using the full Q we would write:

var get = function (id) {
    return Q.all([Db.get(id, "abc"), Db.get(id, "def")])
        .spread(function (res1, res2) {
            return {res1: res1, res2: res2};
        });//the error case is handled automatically
};

In this case, .spread acts like .then except that it spreads the results of an array for a promise out over the arguments of its onFulfilled function. To change this to use the promise methods from AngularJS we just need to make do without .spread. This leads to the following solution:

var get = function (id) {
    return $q.all([Db.get(id, "abc"), Db.get(id, "def")])
        .then(function (res) {
            return {res1: res[0], res2: res[1]};
        });//the error case is handled automatically
};

The beauty of this is that we are freed from handling all the nitty grity of error propagation and storing the partial results because .then acts as a filter. If you leave out the error handler, it automatically propagates any errors. This means that if either of the input promises are rejected, the result will be rejected. If both promises are fulfilled successfully, res is the array of those resolution values.

Questions:
Answers:

I have something to add to @ForbesLindesay answer.

In our case, we wanted partial results: if a request failed (eg. server has an hiccup, we request something deleted by somebody else, etc.), we still want to collect the valid responses, and to report the errors.

I found out that we need to handle success and failure on each promise, returning a value that will be collected by $q.all.

Here is our code, simplified and made generic (‘item’…):

var promiseList = _.map(itemList, function(item)
{
    return DataService.getISubtems(item.id)
        .then(
            function(response)
            {
                var subItems = response.data;
                $log.info('Received sub-item list;' + subItems.length + ';items received');
                return subItems;
            },
            function(reason)
            {
                $log.warn('Sub-item list not received for item;' + item.name + ';' + item.id);
                $scope.errorList.push('Sub-item list not received for item "' + item.name + '"');
            }
        );
});
$q.all(promiseList)
    .then(function(itemArray)
    {
        // We get an array of arrays interleaved with undefined value when an error was raised.
        // That's because error handling doesn't return anything, ie. returns undefined.
        // We remove these undefined values then put all operations at the same level.
        var allOperations = _(operationArray).reject(_.isUndefined).flatten().value();
        if ($scope.errorList.length > 0)
        {
            NotificationService.warning('Items Fetching', 'Errors while getting item list:\n' +
                $scope.errorList.join('\n'));
        }
        $scope._onItemListReceived(allItems);
    });

Note that we use Lodash (_.map, _.flatten, _.reject, _.isUndefined) but I think the usage is quite clear (that’s the nice point of this library!).

Questions:
Answers:

You can use the angular-q-spread library and then use the same code as @ForbesLindesay’s first example:

// The module needs $q-spread as a dependency:
// angular.module('…', ['$q-spread']);

var get = function (id) {
    return $q.all([Db.get(id, "abc"), Db.get(id, "def")])
        .spread(function (res1, res2) {
            return {res1: res1, res2: res2};
        });
};