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

82,001

Solution 1

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

Solution 2

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

Solution 3

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;
}
Share:
82,001
SebastianRiemer
Author by

SebastianRiemer

Personal interests: cycling, skiing, running, gaming, playing guitar Work-related: currently: web development [java, springframework, angularJS, jquery, javascript, CSS, html, hibernate, mysql, jsp] formerly: pl/sql programming [oracle pl/sql, .NET, C#]

Updated on July 09, 2022

Comments

  • SebastianRiemer
    SebastianRiemer almost 2 years

    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.

  • Beetroot-Beetroot
    Beetroot-Beetroot over 10 years
    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.
  • Gruff Bunny
    Gruff Bunny over 10 years
    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).
  • SebastianRiemer
    SebastianRiemer over 10 years
    Thank you for you suggestion @GruffBunny I will look into it asap and let you know!
  • SebastianRiemer
    SebastianRiemer over 10 years
    @GruffBunny Thanks for your explanation! The promises.push ... and $q.all was what I was looking for!
  • Jason
    Jason over 9 years
    How would you go about this if the requests need to be ran sequentially?
  • Michel van Engelen
    Michel van Engelen over 9 years
    @Jason, chain the promises: example.
  • Steven Wexler
    Steven Wexler over 9 years
    @Jason, chaining promises works. If you're looking for a more generic approach you can use $q.serial.
  • DanielM
    DanielM about 9 years
    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.
  • DanielM
    DanielM about 9 years
    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.
  • Roamer-1888
    Roamer-1888 about 9 years
    Are you sure angular.forEach() works like that? Would let promises = $scope.testArray.map(writeSome); not be better?
  • Roamer-1888
    Roamer-1888 about 9 years
    And let results = await Promise.all($scope.testArray.map(writeSome)); is even more compact.
  • Roamer-1888
    Roamer-1888 about 9 years
    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')); }?
  • Maurizio In denmark
    Maurizio In denmark about 9 years
    @Roamer-1888 feel free to edit if it does not work. I think you are right, I haven't fully tested that
  • shoesel
    shoesel almost 8 years
    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.
  • Gruff Bunny
    Gruff Bunny almost 8 years
    @shoesel I think that's what the OP wanted: "..but not before all the previous calls in the for-loop have been resolved."
  • Robbie Smith
    Robbie Smith over 7 years
    I've seen await in C# too. I think they both share the same idea.