108

So I have a situation where I have multiple promise chains of an unknown length. I want some action to run when all the CHAINS have been processed. Is that even possible? Here is an example:

app.controller('MainCtrl', function($scope, $q, $timeout) {
    var one = $q.defer();
    var two = $q.defer();
    var three = $q.defer();

    var all = $q.all([one.promise, two.promise, three.promise]);
    all.then(allSuccess);

    function success(data) {
        console.log(data);
        return data + "Chained";
    }

    function allSuccess(){
        console.log("ALL PROMISES RESOLVED")
    }

    one.promise.then(success).then(success);
    two.promise.then(success);
    three.promise.then(success).then(success).then(success);

    $timeout(function () {
        one.resolve("one done");
    }, Math.random() * 1000);

    $timeout(function () {
        two.resolve("two done");
    }, Math.random() * 1000);

    $timeout(function () {
        three.resolve("three done");
    }, Math.random() * 1000);
});

In this example, I set up a $q.all() for promises one, two, and three which will get resolved at some random time. I then add promises onto the ends of one and three. I want the all to resolve when all the chains have been resolved. Here is the output when I run this code:

one done 
one doneChained
two done
three done
ALL PROMISES RESOLVED
three doneChained
three doneChainedChained 

Is there a way to wait for the chains to resolve?

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
jensengar
  • 6,117
  • 17
  • 58
  • 90

6 Answers6

165

I want the all to resolve when all the chains have been resolved.

Sure, then just pass the promise of each chain into the all() instead of the initial promises:

$q.all([one.promise, two.promise, three.promise]).then(function() {
    console.log("ALL INITIAL PROMISES RESOLVED");
});

var onechain   = one.promise.then(success).then(success),
    twochain   = two.promise.then(success),
    threechain = three.promise.then(success).then(success).then(success);

$q.all([onechain, twochain, threechain]).then(function() {
    console.log("ALL PROMISES RESOLVED");
});
Daniel Kmak
  • 18,164
  • 7
  • 66
  • 89
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • 3
    Thanks for confirming my worst fear. Now I have to come up with a way to get the last promise lol. – jensengar Feb 13 '14 at 18:30
  • What's the problem with that? Are your chains dynamically constructed? – Bergi Feb 13 '14 at 18:35
  • Exactly my problem. I am trying to dynamically create a promise chain then I want to do something when the chain(s) complete. – jensengar Feb 13 '14 at 18:42
  • Can you show us your code (maybe ask a new question)? Are there items appended to the chain after `Q.all` was executed - otherwise it should be trivial? – Bergi Feb 13 '14 at 18:46
  • I would love to show you the code... but I haven't finished writing it yet, however I will do my best to explain it. I have a list of "actions" that need to be done. These actions may have any number levels of sub-actions associated with them. I want to be able to do something when all the actions and their subactions are complete. There will likely be multiple `$q.all`s, however once I start the resolution process, no new actions/promises will be chained. – jensengar Feb 13 '14 at 19:18
  • Yeah, you'd recursively descend the tree of actions with multiple `all()` calls for subactions on each level - then you only need to call `act(rootAction).then(…)` – Bergi Feb 13 '14 at 19:28
  • Exactly, I just haven't gotten that far yet or figured out how to build the `all()` correctly for each level. – jensengar Feb 13 '14 at 19:38
  • s/each level/each node/ - sorry if that has caused some confusion. – Bergi Feb 13 '14 at 19:42
16

The accepted answer is correct. I would like to provide an example to elaborate it a bit to those who aren't familiar with promise.

Example:

In my example, I need to replace the src attributes of img tags with different mirror urls if available before rendering the content.

var img_tags = content.querySelectorAll('img');

function checkMirrorAvailability(url) {

    // blah blah 

    return promise;
}

function changeSrc(success, y, response) {
    if (success === true) {
        img_tags[y].setAttribute('src', response.mirror_url);
    } 
    else {
        console.log('No mirrors for: ' + img_tags[y].getAttribute('src'));
    }
}

var promise_array = [];

for (var y = 0; y < img_tags.length; y++) {
    var img_src = img_tags[y].getAttribute('src');

    promise_array.push(
        checkMirrorAvailability(img_src)
        .then(

            // a callback function only accept ONE argument. 
            // Here, we use  `.bind` to pass additional arguments to the
            // callback function (changeSrc).

            // successCallback
            changeSrc.bind(null, true, y),
            // errorCallback
            changeSrc.bind(null, false, y)
        )
    );
}

$q.all(promise_array)
.then(
    function() {
        console.log('all promises have returned with either success or failure!');
        render(content);
    }
    // We don't need an errorCallback function here, because above we handled
    // all errors.
);

Explanation:

From AngularJS docs:

The then method:

then(successCallback, errorCallback, notifyCallback) – regardless of when the promise was or will be resolved or rejected, then calls one of the success or error callbacks asynchronously as soon as the result is available. The callbacks are called with a single argument: the result or rejection reason.

$q.all(promises)

Combines multiple promises into a single promise that is resolved when all of the input promises are resolved.

The promises param can be an array of promises.

About bind(), More info here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind

Community
  • 1
  • 1
Hieu
  • 7,138
  • 2
  • 42
  • 34
  • The `then` method of `$q.all` is provided an array of the returned promises, so you can loop that array and call `then` on each item in the array, as opposed to calling `then` when you add the promise to `promise_array`. – nick Jan 19 '16 at 15:09
4

Recently had this problem but with unkown number of promises.Solved using jQuery.map().

function methodThatChainsPromises(args) {

    //var args = [
    //    'myArg1',
    //    'myArg2',
    //    'myArg3',
    //];

    var deferred = $q.defer();
    var chain = args.map(methodThatTakeArgAndReturnsPromise);

    $q.all(chain)
    .then(function () {
        $log.debug('All promises have been resolved.');
        deferred.resolve();
    })
    .catch(function () {
        $log.debug('One or more promises failed.');
        deferred.reject();
    });

    return deferred.promise;
}
SoniCue
  • 319
  • 4
  • 6
  • It's not jQuery.map() but Array.prototype.map() (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) but this approach works. – Anastasia Feb 06 '16 at 23:58
0

There is a way. $q.all(...

You can check the below stuffs:

http://jsfiddle.net/ThomasBurleson/QqKuk/

http://denisonluz.com/blog/index.php/2013/10/06/angularjs-returning-multiple-promises-at-once-with-q-all/

Nikola Yovchev
  • 9,498
  • 4
  • 46
  • 72
  • That requires me to know the length of my chain though right? I mean if I had a promise of length 10, I would have to do `$q.all([p1.then(..).then(...).then(...).then(...) ...]);` right? – jensengar Feb 13 '14 at 16:22
0

You can use "await" in an "async function".

app.controller('MainCtrl', async function($scope, $q, $timeout) {
  ...
  var all = await $q.all([one.promise, two.promise, three.promise]); 
  ...
}

NOTE: I'm not 100% sure you can call an async function from a non-async function and have the right results.

That said this wouldn't ever be used on a website. But for load-testing/integration test...maybe.

Example code:

async function waitForIt(printMe) {
  console.log(printMe);
  console.log("..."+await req());
  console.log("Legendary!")
}

function req() {
  
  var promise = new Promise(resolve => {
    setTimeout(() => {
      resolve("DARY!");
    }, 2000);
    
  });

    return promise;
}

waitForIt("Legen-Wait For It");
Flavouski
  • 153
  • 1
  • 11
0

I got this example used in the ocLazyLoad library:

var promises = [];
angular.forEach(paths, function loading(path) {
    promises.push(buildElement('css', path, params));
});
$q.all(promises).then(function success() {
    callback();
 }, function error(err) {
        callback(err);
  });
ajaristi
  • 979
  • 1
  • 9
  • 16