Search code examples
javascriptpromisebluebird

Proper while() loop for bluebird promises (without recursion?)


I've been learning promises using bluebird for two weeks now. I have them mostly understood, but I went to go solve a few related problems and it seems my knowledge has fell apart. I'm trying to do this simple code:

var someGlobal = true;

whilePromsie(function() { 
   return someGlobal;
}, function(result) { // possibly even use return value of 1st parm?
 // keep running this promise code
 return new Promise(....).then(....);
});

as a concrete example:

// This is some very contrived functionality, but let's pretend this is 
// doing something external: ajax call, db call, filesystem call, etc.
// Simply return a number between  0-999 after a 0-999 millisecond
// fake delay.
function getNextItem() { 
    return new Promise.delay(Math.random()*1000).then(function() {
        Promise.cast(Math.floor(Math.random() * 1000));
    });
}

promiseWhile(function() {
    // this will never return false in my example so run forever
    return getNextItem() !== false;
}, // how to have result == return value of getNextItem()? 
function(result) {
    result.then(function(x) { 
        // do some work ... 
    }).catch(function(err) { 
        console.warn("A nasty error occured!: ", err);
    });
}).then(function(result) { 
    console.log("The while finally ended!");
});

Now I've done my homework! There is the same question, but geared toward Q.js here:

Correct way to write loops for promise.

But the accepted answers, as well as additional answers:

  • Are geared toward Q.js or RSVP
  • The only answer geared toward bluebird uses recursion. These seems like it's likely to cause a huge stack overflow in an infinite loop such as mine? Or at best, be very inefficient and create a very large stack for nothing? If I'm wrong, then fine! Let me know.
  • Don't allow you to use result of the condition. Although this isn't requirement -- I'm just curious if it's possible. The code I'm writing, one use case needs it, the other doesn't.

Now, there is an answer regarding RSVP that uses this async() method. And what really confuses me is bluebird documents and I even see code for a Promise.async() call in the repository, but I don't see it in my latest copy of bluebird. Is it in the git repository only or something?


Solution

  • It's not 100% clear what you're trying to do, but I'll write an answer that does the following things you mention:

    1. Loops until some condition in your code is met
    2. Allows you to use a delay between loop iterations
    3. Allows you to get and process the final result
    4. Works with Bluebird (I'll code to the ES6 promise standard which will work with Bluebird or native promises)
    5. Does not have stack build-up

    First, let's assume you have some async function that returns a promise whose result is used to determine whether to continue looping or not.

    function getNextItem() {
       return new Promise.delay(Math.random()*1000).then(function() {
            return(Math.floor(Math.random() * 1000));
       });
    }
    

    Now, you want to loop until the value returned meets some condition

    function processLoop(delay) {
        return new Promise(function(resolve, reject) {
            var results = [];
    
            function next() {
                getNextItem().then(function(val) {
                    // add to result array
                    results.push(val);
                    if (val < 100) {
                        // found a val < 100, so be done with the loop
                        resolve(results);
                    } else {
                        // run another iteration of the loop after delay
                        setTimeout(next, delay);
                    }
                }, reject);
            }
            // start first iteration of the loop
            next();
        });
    }
    
    processLoop(100).then(function(results) {
       // process results here
    }, function(err) {
       // error here
    });
    

    If you wanted to make this more generic so you could pass in the function and comparison, you could do this:

    function processLoop(mainFn, compareFn, delay) {
        return new Promise(function(resolve, reject) {
            var results = [];
    
            function next() {
                mainFn().then(function(val) {
                    // add to result array
                    results.push(val);
                    if (compareFn(val))
                        // found a val < 100, so be done with the loop
                        resolve(results);
                    } else {
                        // run another iteration of the loop after delay
                        if (delay) {
                            setTimeout(next, delay);
                        } else {
                            next();
                        }
                    }
                }, reject);
            }
            // start first iteration of the loop
            next();
        });
    }
    
    processLoop(getNextItem, function(val) {
        return val < 100;
    }, 100).then(function(results) {
       // process results here
    }, function(err) {
       // error here
    });
    

    Your attempts at a structure like this:

    return getNextItem() !== false;
    

    Can't work because getNextItem() returns a promise which is always !== false since a promise is an object so that can't work. If you want to test a promise, you have to use .then() to get its value and you have to do the comparson asynchronously so you can't directly return a value like that.


    Note: While these implementations use a function that calls itself, this does not cause stack build-up because they call themselves asynchronously. That means the stack has already completely unwound before the function calls itself again, thus there is no stack build-up. This will always be the case from a .then() handler since the Promise specification requires that a .then() handler is not called until the stack has returned to "platform code" which means it has unwound all regular "user code" before calling the .then() handler.


    Using async and await in ES7

    In ES7, you can use async and await to "pause" a loop. That can make this type of iteration a lot simpler to code. This looks structurally more like a typical synchronous loop. It uses await to wait on promises and because the function is declared async, it always returns a promise:

    function delay(t) {
        return new Promise(resolve => {
            setTimeout(resolve, t);
        });
    }
    
    async function processLoop(mainFn, compareFn, timeDelay) {
        var results = [];
    
        // loop until condition is met
        while (true) {
            let val = await mainFn();
            results.push(val);
            if (compareFn(val)) {
                return results;
            } else {
                if (timeDelay) {
                    await delay(timeDelay);
                }
            }
        }
    }
    
    processLoop(getNextItem, function(val) {
        return val < 100;
    }, 100).then(function(results) {
       // process results here
    }, function(err) {
       // error here
    });