Correct way to write loops for promise.

user2127480 picture user2127480 · Jul 9, 2014 · Viewed 135.9k times · Source

How to correctly construct a loop to make sure the following promise call and the chained logger.log(res) runs synchronously through iteration? (bluebird)

db.getUser(email).then(function(res) { logger.log(res); }); // this is a promise

I tried the following way (method from http://blog.victorquinn.com/javascript-promise-while-loop )

var Promise = require('bluebird');

var promiseWhile = function(condition, action) {
    var resolver = Promise.defer();

    var loop = function() {
        if (!condition()) return resolver.resolve();
        return Promise.cast(action())
            .then(loop)
            .catch(resolver.reject);
    };

    process.nextTick(loop);

    return resolver.promise;
});

var count = 0;
promiseWhile(function() {
    return count < 10;
}, function() {
    return new Promise(function(resolve, reject) {
        db.getUser(email)
          .then(function(res) { 
              logger.log(res); 
              count++;
              resolve();
          });
    }); 
}).then(function() {
    console.log('all done');
}); 

Although it seems to work, but I don't think it guarantees the order of calling logger.log(res);

Any suggestions?

Answer

Roamer-1888 picture Roamer-1888 · Jul 27, 2014

If you really want a general promiseWhen() function for this and other purposes, then by all means do so, using Bergi's simplifications. However, because of the way promises work, passing callbacks in this way is generally unnecessary and forces you to jump through complex little hoops.

As far as I can tell you're trying :

  • to asynchronously fetch a series of user details for a collection of email addresses (at least, that's the only scenario that makes sense).
  • to do so by building a .then() chain via recursion.
  • to maintain the original order when handling the returned results.

Defined thus, the problem is actually the one discussed under "The Collection Kerfuffle" in Promise Anti-patterns, which offers two simple solutions :

  • parallel asynchronous calls using Array.prototype.map()
  • serial asynchronous calls using Array.prototype.reduce().

The parallel approach will (straightforwardly) give the issue that you are trying to avoid - that the order of the responses is uncertain. The serial approach will build the required .then() chain - flat - no recursion.

function fetchUserDetails(arr) {
    return arr.reduce(function(promise, email) {
        return promise.then(function() {
            return db.getUser(email).done(function(res) {
                logger.log(res);
            });
        });
    }, Promise.resolve());
}

Call as follows :

//Compose here, by whatever means, an array of email addresses.
var arrayOfEmailAddys = [...];

fetchUserDetails(arrayOfEmailAddys).then(function() {
    console.log('all done');
});

As you can see, there's no need for the ugly outer var count or it's associated condition function. The limit (of 10 in the question) is determined entirely by the length of the array arrayOfEmailAddys.