angular $q, How to chain multiple promises within and after a for-loop
Asked Answered
A

4

74

I want to have a for-loop which calls async functions each iteration.

After the for-loop I want to execute another code block, but not before all the previous calls in the for-loop have been resolved.

My problem at the moment is, that either the code-block after the for-loop is executed before all async calls have finished OR it is not executed at all.

The code part with the FOR-loop and the code block after it (for complete code, please see fiddle):

[..]
function outerFunction($q, $scope) {
    var defer = $q.defer();    
    readSome($q,$scope).then(function() {
        var promise = writeSome($q, $scope.testArray[0])
        for (var i=1; i < $scope.testArray.length; i++) {
             promise = promise.then(
                 angular.bind(null, writeSome, $q, $scope.testArray[i])
             );                                  
        } 
        // this must not be called before all calls in for-loop have finished
        promise = promise.then(function() {
            return writeSome($q, "finish").then(function() {
                console.log("resolve");
                // resolving here after everything has been done, yey!
                defer.resolve();
            });   
        });        
    });   

    return defer.promise;
}

I've created a jsFiddle which can be found here http://jsfiddle.net/riemersebastian/B43u6/3/.

At the moment it looks like the execution order is fine (see the console output).

My guess is, that this is simply because every function call returns immediately without doing any real work. I have tried to delay the defer.resolve with setTimeout but failed (i.e. the last code block was never executed). You can see it in the outcommented block in the fiddle.

When I use the real functions which write to file and read from file, the last code block is executed before the last write operation finishes, which is not what I want.

Of course, the error could be in one of those read/write functions, but I would like to verify that there is nothing wrong with the code I have posted here.

Acidosis answered 9/1, 2014 at 15:31 Comment(4)
(1) About the functions you are calling from inside the loop: do they have to run sequentially or are they parallel, still requiring the last block to run after all of them have finished? And: (2) What should happen if one of them results in an error?Rogation
If you're using a write function, those are often asynchronous as well, so it's very possible that everything is working "as intended."; that is, angular is kicking off all the writes (which takes a fraction of time), but the writes themselves are taking a long time. What are you writing, and what API are you using?Centrifugal
@NikosParaskevopoulos (1) I don't really care whether they run in parallel or sequentially, they could be run in parallel as they do not depend on each other. As for now, each inner function returns a promise and resolves at the end of the operation, meaning they execute in serial. You got it, the last operation must always be the last operation executed, regardlesss whether the previous ran in parallel or in serial. (2) Good question, I guess some warning could be logged but that's not that important.Acidosis
@Centrifugal I am writing a JSONObject to file and I use chromes' filesystem for storage. I guess the most important part of that is, that I resolve the defer within fileWriter.onwriteend, fileWriter.onerror, etc.Acidosis
P
120

What you need to use is $q.all which combines a number of promises into one which is only resolved when all the promises are resolved.

In your case you could do something like:

function outerFunction() {

    var defer = $q.defer();
    var promises = [];

    function lastTask(){
        writeSome('finish').then( function(){
            defer.resolve();
        });
    }

    angular.forEach( $scope.testArray, function(value){
        promises.push(writeSome(value));
    });

    $q.all(promises).then(lastTask);

    return defer.promise;
}
Putt answered 10/1, 2014 at 23:28 Comment(12)
Out of interest, does Angualr provide for $q.defer().resolve() to be detached, as in jQuery? In other words, could you write writeSome('finish').then(defer.resolve);? If so the code would be slightly more compact but otherwise identical.Inseminate
Good suggestion. The 'then' function takes a function that will be called when the promise is resolved, so yes passing a parameter of defer.resolve will work. I'll leave the answer as it is for now as the question also had some logging in there (which I've omitted for clarity).Putt
Thank you for you suggestion @GruffBunny I will look into it asap and let you know!Acidosis
@GruffBunny Thanks for your explanation! The promises.push ... and $q.all was what I was looking for!Acidosis
How would you go about this if the requests need to be ran sequentially?Randolf
@Jason, chain the promises: example.Swaziland
@Jason, chaining promises works. If you're looking for a more generic approach you can use $q.serial.Rafaelof
Could you please explain why to return defer instead of defer.promise, as usual? It looks a little bit odd to me, since you call resolve() later to resolve the promise, but you don't return it.Torry
I first tested it but didn't really need to make any action on resolve, so it was working. But I found now the answer to my concern: the returned value should actually be defer.promise; otherwise you would get a "then() is not defined" error.Torry
Why not purge the deferred anti-pattern and compact down to function outerFunction() { return $q.all($scope.testArray.map(writeSome)).then(writeSome.bind(null, 'finish')); }?Troy
Only issue is that the chain will stop when the first promise is rejected..that's what $q.all is doing, it resolves ONLY if all promises resolve.Trembles
@Trembles I think that's what the OP wanted: "..but not before all the previous calls in the for-loop have been resolved."Putt
D
3

With the new ES7 you can have the same result in a much more straightforward way:

let promises =  angular.forEach( $scope.testArray, function(value){
    writeSome(value);
});

let results = await Promise.all(promises);

console.log(results);
Dialogism answered 1/6, 2015 at 8:29 Comment(4)
Are you sure angular.forEach() works like that? Would let promises = $scope.testArray.map(writeSome); not be better?Troy
And let results = await Promise.all($scope.testArray.map(writeSome)); is even more compact.Troy
@Troy feel free to edit if it does not work. I think you are right, I haven't fully tested thatDialogism
I've seen await in C# too. I think they both share the same idea.Mccullum
D
1

You can use $q and 'reduce' together, to chain the promises.

function setAutoJoin() {
    var deferred = $q.defer(), data;
    var array = _.map(data, function(g){
            return g.id;
        });

    function waitTillAllCalls(arr) {
        return arr.reduce(function(deferred, email) {
            return somePromisingFnWhichReturnsDeferredPromise(email);
        }, deferred.resolve('done'));
    }

    waitTillAllCalls(array);

    return deferred.promise;
}
Discordant answered 21/4, 2017 at 13:12 Comment(0)
E
0

This worked for me using the ES5 syntax

function outerFunction(bookings) {

    var allDeferred = $q.defer();
    var promises = [];

    lodash.map(bookings, function(booking) {
        var deferred = $q.defer();

        var query = {
            _id: booking.product[0].id,
            populate: true
        }

        Stamplay.Object("product").get(query)
        .then(function(res) {
            booking.product[0] = res.data[0];
            deferred.resolve(booking)
        })
        .catch(function(err) {
            console.error(err);
            deferred.reject(err);
        });

        promises.push(deferred.promise);
    });

    $q.all(promises)
    .then(function(results) { allDeferred.resolve(results) })
    .catch(function(err) { allDeferred.reject(results) });

    return allDeferred.promise;
}
Emelina answered 4/11, 2017 at 1:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.