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.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
SebastianRiemer
  • 1,495
  • 2
  • 20
  • 33
  • (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? – Nikos Paraskevopoulos Jan 09 '14 at 15:55
  • 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? – Hylianpuffball Jan 09 '14 at 15:57
  • @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. – SebastianRiemer Jan 15 '14 at 11:07
  • @Hylianpuffball 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. – SebastianRiemer Jan 15 '14 at 11:19

4 Answers4

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;
}
georgeawg
  • 48,608
  • 13
  • 72
  • 95
Gruff Bunny
  • 27,738
  • 10
  • 72
  • 59
  • 1
    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. – Beetroot-Beetroot Jan 11 '14 at 19:11
  • 1
    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). – Gruff Bunny Jan 11 '14 at 20:11
  • Thank you for you suggestion @GruffBunny I will look into it asap and let you know! – SebastianRiemer Jan 15 '14 at 11:20
  • @GruffBunny Thanks for your explanation! The promises.push ... and $q.all was what I was looking for! – SebastianRiemer Jan 21 '14 at 15:02
  • How would you go about this if the requests need to be ran sequentially? – Jason Nov 26 '14 at 21:52
  • 1
    @Jason, chain the promises: [example](http://www.dwmkerr.com/promises-in-angularjs-the-definitive-guide/#advancedpromiseschaining). – Michel van Engelen Jan 08 '15 at 12:44
  • @Jason, chaining promises works. If you're looking for a more generic approach you can use [$q.serial](http://www.codeducky.org/q-serial/). – Steven Wexler Mar 24 '15 at 04:17
  • 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 May 05 '15 at 10:36
  • 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. – DanielM May 18 '15 at 14:21
  • 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')); }`? – Roamer-1888 Jun 01 '15 at 10:41
  • 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. – shoesel Jul 14 '16 at 11:20
  • @shoesel I think that's what the OP wanted: "..but not before all the previous calls in the for-loop have been resolved." – Gruff Bunny Jul 14 '16 at 11:25
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);
Maurizio In denmark
  • 4,226
  • 2
  • 30
  • 64
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;
}
STEEL
  • 8,955
  • 9
  • 67
  • 89
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;
}
Ben Cochrane
  • 3,317
  • 1
  • 14
  • 16