While loop with promises
Asked Answered
L

14

82

What would be the idiomatic way to do something like a while loop with promises. So:

do something if the condition still stands do it again repeat then do something else.

dosomething.then(possilblydomoresomethings).then(finish)

I've done it this way I was wondering if there were any better/more idomatic ways?

var q = require('q');

var index = 1;

var useless =  function(){
        var currentIndex = index;
        console.log(currentIndex)
        var deferred = q.defer();
        setTimeout(function(){
            if(currentIndex > 10)
                deferred.resolve(false);
            else deferred.resolve(true);
            },500);
        return deferred.promise;
    }

var control = function(cont){
        var deferred = q.defer();
        if(cont){
                index = index + 1;
                useless().then(control).then(function(){
                        deferred.resolve();
                    });
            }
         else deferred.resolve();
        return deferred.promise;
    }

var chain = useless().then(control).then(function(){console.log('done')});

Output: 1 2 3 4 5 6 7 8 9 10 11 done

Lorrinelorry answered 20/6, 2013 at 15:28 Comment(4)
"I was wondering if there were any better/more idomatic ways?" No, recursion is the way to go.Pluralize
How would you do it without recursion? I got the feeling recurison was probably not cool, but I couldn't figure out how to do it without. Any insights?Lorrinelorry
I don't think you can use iteration without some sort of "stop here" mechanism like Taskjs.Pluralize
You can avoid recursion if you have async/await available in your environment; see my updated answer below.Habile
P
19

I'd use an object to wrap the value. That way you can have a done property to let the loop know you're done.

// fn should return an object like
// {
//   done: false,
//   value: foo
// }
function loop(promise, fn) {
  return promise.then(fn).then(function (wrapper) {
    return !wrapper.done ? loop(Q(wrapper.value), fn) : wrapper.value;
  });
}

loop(Q.resolve(1), function (i) {
  console.log(i);
  return {
    done: i > 10,
    value: i++
  };
}).done(function () {
  console.log('done');
});
Pluralize answered 21/6, 2013 at 15:31 Comment(4)
Note that this will probably consume all available memory after a long enough time running; there appears to be something retained in each loop of the promise, at least with Q.Dictation
@Pluralize in your example you should change i++ by ++i otherwise you'll get an "infinite loop".Hydrostatic
I've used this solution successfully, even using require ('promise'); library. I'm now wondering though if a non-recursive solution could be built, see #36362327Dorseydorsiferous
Ashe, could you be be more specific on the memory problem? Is it with wrapper?Laplante
P
61

Here's a reusable function that I think is pretty clear.

var Q = require("q");

// `condition` is a function that returns a boolean
// `body` is a function that returns a promise
// returns a promise for the completion of the loop
function promiseWhile(condition, body) {
    var done = Q.defer();

    function loop() {
        // When the result of calling `condition` is no longer true, we are
        // done.
        if (!condition()) return done.resolve();
        // Use `when`, in case `body` does not return a promise.
        // When it completes loop again otherwise, if it fails, reject the
        // done promise
        Q.when(body(), loop, done.reject);
    }

    // Start running the loop in the next tick so that this function is
    // completely async. It would be unexpected if `body` was called
    // synchronously the first time.
    Q.nextTick(loop);

    // The promise
    return done.promise;
}


// Usage
var index = 1;
promiseWhile(function () { return index <= 11; }, function () {
    console.log(index);
    index++;
    return Q.delay(500); // arbitrary async
}).then(function () {
    console.log("done");
}).done();
Privateer answered 21/6, 2013 at 15:18 Comment(7)
This is great! I ported your example for RSVP.js: jsfiddle.net/wcW4r/1 It might be useful for Ember.js users.Cipango
New version more RSVP idiomatic and with Promise "wrappers" in body and condition: jsfiddle.net/wcW4r/3Cipango
This can swallow exceptions. It's better to use Q.fcall(body).then(loop,done.reject);Depreciable
Strange, this gives me an error : Error: ReferenceError: setTimeout is not defined at flush (vendor/q.js:121:21)Whale
This relies on recursion, so how will it scale then? What happens when trying to make a 1000 calls?Raffia
Because each promise runs in the next "tick of the event loop" the stack gets cleared after every iteration, and so typical issues with recursion (such as stack overflows) do not apply here.Privateer
so I take it with async methods, you should never put them inside a syncronous for loop or for that matter never wrap any async code with sync code.Tybi
H
51

This is the simplest way I've found to express the basic pattern: you define a function that calls the promise, checks its result, and then either calls itself again or terminates.

const doSomething = value =>
  new Promise(resolve => 
    setTimeout(() => resolve(value >= 5 ? 'ok': 'no'), 1000))

const loop = value =>
  doSomething(value).then(result => {
    console.log(value)
    if (result === 'ok') {
      console.log('yay')      
    } else {
      return loop(value + 1)
    }
  })

loop(1).then(() => console.log('all done!'))

See it in action on JSBin

If you were using a promise that resolves or rejects, you would define then and catch instead of using an if-clause.

If you had an array of promises, you would just change loop to shift or pop the next one each time.


EDIT: Here's a version that uses async/await, because it's 2018:

const loop = async value => {
  let result = null
  while (result != 'ok') {
    console.log(value)
    result = await doSomething(value)
    value = value + 1
  }
  console.log('yay')
}

See it in action on CodePen

As you can see, it uses a normal while loop and no recursion.

Habile answered 15/7, 2016 at 1:44 Comment(2)
Very nice solution! I adapted this to pass another variable through the method chain to each promise. Thanks for a great simple example!Veg
more helpful than other answersCripps
P
19

I'd use an object to wrap the value. That way you can have a done property to let the loop know you're done.

// fn should return an object like
// {
//   done: false,
//   value: foo
// }
function loop(promise, fn) {
  return promise.then(fn).then(function (wrapper) {
    return !wrapper.done ? loop(Q(wrapper.value), fn) : wrapper.value;
  });
}

loop(Q.resolve(1), function (i) {
  console.log(i);
  return {
    done: i > 10,
    value: i++
  };
}).done(function () {
  console.log('done');
});
Pluralize answered 21/6, 2013 at 15:31 Comment(4)
Note that this will probably consume all available memory after a long enough time running; there appears to be something retained in each loop of the promise, at least with Q.Dictation
@Pluralize in your example you should change i++ by ++i otherwise you'll get an "infinite loop".Hydrostatic
I've used this solution successfully, even using require ('promise'); library. I'm now wondering though if a non-recursive solution could be built, see #36362327Dorseydorsiferous
Ashe, could you be be more specific on the memory problem? Is it with wrapper?Laplante
F
13

This is for bluebird not q but since you didn't mention q specifically.. in the bluebird api doc the author mentions returning a promise-generating function would be more idiomatic than using deferreds.

var Promise = require('bluebird');
var i = 0;

var counter = Promise.method(function(){
    return i++;
})

function getAll(max, results){
    var results = results || [];
    return counter().then(function(result){
        results.push(result);
        return (result < max) ? getAll(max, results) : results
    })
}

getAll(10).then(function(data){
    console.log(data);
})
Favien answered 17/6, 2014 at 17:55 Comment(3)
This was helpful, use a recursive function to implement the while loop. Thanks.Pitman
I found it useful also to see this, but I'm concerned (for my use - iterating all redis keys with SCAN) that the recursion will generate too much stack and either fail for large data sets, or consume unnecessary memory. I'm thinking es6 generators may be the route I need to take.Blueweed
This is not exactly what I was looking for but helped me finding a solution.Binette
M
5

Since I can't comment on Stuart K's answer I'll add a little bit here. Based on Stuart K's answer you can boil it down to a surprisingly simple concept: Reuse an unfulfilled promise. What he has is essentially:

  1. Create a new instance of a deferred promise
  2. Define your function that you want to call in a loop
  3. Inside that function:
    1. Check to see if you're done; and when you are resolve the promise created in #1 and return it.
    2. If you are not done then tell Q to use the existing promise and run the unfullfilled function that is the "recursive" function, or fail if it died. Q.when(promise, yourFunction, failFunction)
  4. After defining your function use Q to trigger the function for the first time using Q.nextTick(yourFunction)
  5. Finally return your new promise to the caller (which will trigger the whole thing to start).

Stuart's answer is for a more generic solution, but the basics are awesome (once you realize how it works).

Mckale answered 7/5, 2015 at 21:52 Comment(0)
D
4

This pattern is now more easily called by using q-flow. An example, for the above problem:

var q = require('q');
require('q-flow');
var index = 1;
q.until(function() {
  return q.delay(500).then(function() {
    console.log(index++);
    return index > 10;
  });
}).done(function() {
  return console.log('done');
});
Drifter answered 14/5, 2014 at 21:58 Comment(1)
This is great, is it possible to do this with bluebird so I don't need to use 2 different promise libraries together?Nolpros
S
3

Here is an extensions to the Promise prototype to mimic the behavior of a for loop. It supports promises or immediate values for the initialization, condition, loop body, and increment sections. It also has full support for exceptions, and it does not have memory leaks. An example is given below on how to use it.

var Promise = require('promise');


// Promise.loop([properties: object]): Promise()
//
//  Execute a loop based on promises. Object 'properties' is an optional
//  argument with the following fields:
//
//  initialization: function(): Promise() | any, optional
//
//      Function executed as part of the initialization of the loop. If
//      it returns a promise, the loop will not begin to execute until
//      it is resolved.
//
//      Any exception occurring in this function will finish the loop
//      with a rejected promise. Similarly, if this function returns a
//      promise, and this promise is reject, the loop finishes right
//      away with a rejected promise.
//
//  condition: function(): Promise(result: bool) | bool, optional
//
//      Condition evaluated in the beginning of each iteration of the
//      loop. The function should return a boolean value, or a promise
//      object that resolves with a boolean data value.
//
//      Any exception occurring during the evaluation of the condition
//      will finish the loop with a rejected promise. Similarly, it this
//      function returns a promise, and this promise is rejected, the
//      loop finishes right away with a rejected promise.
//
//      If no condition function is provided, an infinite loop is
//      executed.
//
//  body: function(): Promise() | any, optional
//
//      Function acting as the body of the loop. If it returns a
//      promise, the loop will not proceed until this promise is
//      resolved.
//
//      Any exception occurring in this function will finish the loop
//      with a rejected promise. Similarly, if this function returns a
//      promise, and this promise is reject, the loop finishes right
//      away with a rejected promise.
//
//  increment: function(): Promise() | any, optional
//
//      Function executed at the end of each iteration of the loop. If
//      it returns a promise, the condition of the loop will not be
//      evaluated again until this promise is resolved.
//
//      Any exception occurring in this function will finish the loop
//      with a rejected promise. Similarly, if this function returns a
//      promise, and this promise is reject, the loop finishes right
//      away with a rejected promise.
//
Promise.loop = function(properties)
{
    // Default values
    properties = properties || {};
    properties.initialization = properties.initialization || function() { };
    properties.condition = properties.condition || function() { return true; };
    properties.body = properties.body || function() { };
    properties.increment = properties.increment || function() { };

    // Start
    return new Promise(function(resolve, reject)
    {
        var runInitialization = function()
        {
            Promise.resolve().then(function()
            {
                return properties.initialization();
            })
            .then(function()
            {
                process.nextTick(runCondition);
            })
            .catch(function(error)
            {
                reject(error);
            });
        }

        var runCondition = function()
        {
            Promise.resolve().then(function()
            {
                return properties.condition();
            })
            .then(function(result)
            {
                if (result)
                    process.nextTick(runBody);
                else
                    resolve();
            })
            .catch(function(error)
            {
                reject(error);
            });
        }

        var runBody = function()
        {
            Promise.resolve().then(function()
            {
                return properties.body();
            })
            .then(function()
            {
                process.nextTick(runIncrement);
            })
            .catch(function(error)
            {
                reject(error);
            });
        }

        var runIncrement = function()
        {
            Promise.resolve().then(function()
            {
                return properties.increment();
            })
            .then(function()
            {
                process.nextTick(runCondition);
            })
            .catch(function(error)
            {
                reject(error);
            });
        }

        // Start running initialization
        process.nextTick(runInitialization);
    });
}


// Promise.delay(time: double): Promise()
//
//  Returns a promise that resolves after the given delay in seconds.
//
Promise.delay = function(time)
{
    return new Promise(function(resolve)
    {
        setTimeout(resolve, time * 1000);
    });
}


// Example
var i;
Promise.loop({
    initialization: function()
    {
        i = 2;
    },
    condition: function()
    {
        return i < 6;
    },
    body: function()
    {
        // Print "i"
        console.log(i);

        // Exception when 5 is reached
        if (i == 5)
            throw Error('Value of "i" reached 5');

        // Wait 1 second
        return Promise.delay(1);
    },
    increment: function()
    {
        i++;
    }
})
.then(function()
{
    console.log('LOOP FINISHED');
})
.catch(function(error)
{
    console.log('EXPECTED ERROR:', error.message);
});
Skep answered 25/12, 2014 at 20:44 Comment(0)
H
2

Here is a generic solution that uses ES6 promises:

/**
 * Simulates a while loop where the condition is determined by the result of a Promise.
 *
 * @param {Function} condition
 * @param {Function} action
 * @returns {Promise}
 */
function promiseWhile (condition, action) {
    return new Promise((resolve, reject) => {
        const loop = function () {
            if (!condition()) {
                resolve();
            } else {
                Promise.resolve(action())
                    .then(loop)
                    .catch(reject);
            }
        }
        loop();
    })
}

/**
 * Simulates a do-while loop where the condition is determined by the result of a Promise.
 *
 * @param {Function} condition
 * @param {Function} action
 * @returns {Promise}
 */
function promiseDoWhile (condition, action) {
    return Promise.resolve(action())
        .then(() => promiseWhile(condition, action));
}

export default promiseWhile;
export {promiseWhile, promiseDoWhile};

And you can use it like this:

let myCounter = 0;

function myAsyncFunction () {
    return new Promise(resolve => {
        setTimeout(() => {
            console.log(++myCounter);
            resolve()
        }, 1000)
    });
}


promiseWhile(() => myCounter < 5, myAsyncFunction).then(() => console.log(`Timer completed: ${myCounter}`));
Hellenhellene answered 3/3, 2021 at 18:7 Comment(0)
P
1
var Q = require('q')

var vetor  = ['a','b','c']

function imprimeValor(elements,initValue,defer){

    console.log( elements[initValue++] )
    defer.resolve(initValue)
    return defer.promise
}

function Qloop(initValue, elements,defer){

    Q.when( imprimeValor(elements, initValue, Q.defer()), function(initValue){

        if(initValue===elements.length){
            defer.resolve()
        }else{
            defer.resolve( Qloop(initValue,elements, Q.defer()) )
        }
    }, function(err){

        defer.reject(err)
    })

    return defer.promise
}

Qloop(0, vetor,Q.defer())
Pearcy answered 18/2, 2016 at 18:16 Comment(0)
F
1

I am now using this:

function each(arr, work) {
  function loop(arr, i) {
    return new Promise(function(resolve, reject) {
      if (i >= arr.length) {resolve();}
      else try {
        Promise.resolve(work(arr[i], i)).then(function() { 
          resolve(loop(arr, i+1))
        }).catch(reject);
      } catch(e) {reject(e);}
    });
  }
  return loop(arr, 0);
}

This accepts an array arr and a function work and returns a Promise. The supplied function gets called once for each element in the array and gets passed the current element and it's index in the array. It may be sync or async, in which case it must return a Promise.

You can use it like this:

var items = ['Hello', 'cool', 'world'];
each(items, function(item, idx) {
    // this could simply be sync, but can also be async
    // in which case it must return a Promise
    return new Promise(function(resolve){
        // use setTimeout to make this async
        setTimeout(function(){
            console.info(item, idx);
            resolve();
        }, 1000);
    });
})
.then(function(){
    console.info('DONE');
})
.catch(function(error){
    console.error('Failed', error);
})

Each item in the array will be handled in turn. Once all are handled, the code given to .then() will run, or, if some error occurred, the code given to .catch(). Inside the work function, you can throw an Error (in case of synchronous functions) or reject the Promise (in case of async functions) to abort the loop.

function each(arr, work) {
  function loop(arr, i) {
    return new Promise(function(resolve, reject) {
      if (i >= arr.length) {resolve();}
      else try {
        Promise.resolve(work(arr[i], i)).then(function() { 
          resolve(loop(arr, i+1))
        }).catch(reject);
      } catch(e) {reject(e);}
    });
  }
  return loop(arr, 0);
}

var items = ['Hello', 'cool', 'world'];
each(items, function(item, idx) {
  // this could simply be sync, but can also be async
  // in which case it must return a Promise
  return new Promise(function(resolve){
    // use setTimeout to make this async
    setTimeout(function(){
      console.info(item, idx);
      resolve();
    }, 1000);
  });
})
.then(function(){
  console.info('DONE');
})
.catch(function(error){
  console.error('Failed', error);
})
Frizzly answered 18/6, 2016 at 18:53 Comment(0)
L
1

Lots of answers here and what you are trying to achieve is not very practical. but this should work. This was implemented in an aws lambda function, with Node.js 10 it will go until function timeout. It may also consume a decent amount of memory.

exports.handler = async (event) => {
  let res = null;
  while (true) {
    try{
     res = await dopromise();
    }catch(err){
     res = err;
    }
    console.log(res);
   }//infinite will time out
  };

  function dopromise(){
   return new Promise((resolve, reject) => {
    //do some logic
    //if error reject
        //reject('failed');
    resolve('success');
  });
}

Tested on lambda and running fine for over 5 min. But as stated by others this is not a good thing to do.

Lotion answered 31/5, 2019 at 15:38 Comment(0)
V
0

Using the ES6 Promise, I came up with this. It chains the promises and returns a promise. It's not technically a while loop, but does show how to iterate over promises synchronously.

function chain_promises(list, fun) {
    return list.reduce(
        function (promise, element) {
            return promise.then(function () {
                // I only needed to kick off some side-effects. If you need to get
                // a list back, you would append to it here. Or maybe use
                // Array.map instead of Array.reduce.
                fun(element);
            });
    	},
        // An initial promise just starts things off.
        Promise.resolve(true)
    );
}

// To test it...

function test_function (element) {
    return new Promise(function (pass, _fail) {
        console.log('Processing ' + element);
        pass(true);
    });
}

chain_promises([1, 2, 3, 4, 5], test_function).then(function () {
    console.log('Done.');
});

Here's my fiddle.

Vincenzovincible answered 11/11, 2015 at 19:8 Comment(1)
Tip: Use Promise.resolve(true) instead of the new Promise constructorPerfunctory
C
0

I thought I might as well throw my hat in the ring, using ES6 Promises...

function until_success(executor){
    var before_retry = undefined;
    var outer_executor = function(succeed, reject){
        var rejection_handler = function(err){
            if(before_retry){
                try {
                    var pre_retry_result = before_retry(err);
                    if(pre_retry_result)
                        return succeed(pre_retry_result);
                } catch (pre_retry_error){
                    return reject(pre_retry_error);
                }
            }
            return new Promise(executor).then(succeed, rejection_handler);                
        }
        return new Promise(executor).then(succeed, rejection_handler);
    }

    var outer_promise = new Promise(outer_executor);
    outer_promise.before_retry = function(func){
        before_retry = func;
        return outer_promise;
    }
    return outer_promise;
}

The executor argument is the same as that passed to a Promise constructor, but will be called repeatedly until it triggers the success callback. The before_retry function allows for custom error handling on the failed attempts. If it returns a truthy value it will be considered a form of success and the "loop" will end, with that truthy as the result. If no before_retry function is registered, or it returns a falsey value, then the loop will run for another iteration. The third option is that the before_retry function throws an error itself. If this happens, then the "loop" will end, passing that error as an error.


Here is an example:

var counter = 0;
function task(succ, reject){
    setTimeout(function(){
        if(++counter < 5)
            reject(counter + " is too small!!");
        else
            succ(counter + " is just right");
    }, 500); // simulated async task
}

until_success(task)
        .before_retry(function(err){
            console.log("failed attempt: " + err);
            // Option 0: return falsey value and move on to next attempt
            // return

            // Option 1: uncomment to get early success..
            //if(err === "3 is too small!!") 
            //    return "3 is sort of ok"; 

            // Option 2: uncomment to get complete failure..
            //if(err === "3 is too small!!") 
            //    throw "3rd time, very unlucky"; 
  }).then(function(val){
       console.log("finally, success: " + val);
  }).catch(function(err){
       console.log("it didn't end well: " + err);
  })

Output for option 0:

failed attempt: 1 is too small!!
failed attempt: 2 is too small!!
failed attempt: 3 is too small!!
failed attempt: 4 is too small!!
finally, success: 5 is just right

Output for option 1:

failed attempt: 1 is too small!!
failed attempt: 2 is too small!!
failed attempt: 3 is too small!!
finally, success: 3 is sort of ok

Output for option 2:

failed attempt: 1 is too small!!
failed attempt: 2 is too small!!
failed attempt: 3 is too small!!
it didn't end well: 3rd time, very unlucky
Cleodal answered 3/3, 2016 at 20:32 Comment(0)
S
-1

I wrote a module which helps you do chained loops of asynchronous tasks with promises, it is based on the answer above provided by juandopazo

/**
 * Should loop over a task function which returns a "wrapper" object
 * until wrapper.done is true. A seed value wrapper.seed is propagated to the
 * next run of the loop.
 *
 * todo/maybe? Reject if wrapper is not an object with done and seed keys.
 *
 * @param {Promise|*} seed
 * @param {Function} taskFn
 *
 * @returns {Promise.<*>}
 */
function seedLoop(seed, taskFn) {
  const seedPromise = Promise.resolve(seed);

  return seedPromise
    .then(taskFn)
    .then((wrapper) => {
      if (wrapper.done) {
        return wrapper.seed;
      }

      return seedLoop(wrapper.seed, taskFn);
    });
}

// A super simple example of counting to ten, which doesn't even
// do anything asynchronous, but if it did, it should resolve to 
// a promise that returns the { done, seed } wrapper object for the
// next call of the countToTen task function.
function countToTen(count) {
  const done = count > 10;
  const seed = done ? count : count + 1;

  return {done, seed};
}

seedLoop(1, countToTen).then((result) => {
  console.log(result); // 11, the first value which was over 10.
});

https://github.com/CascadeEnergy/promise-seedloop

Seeing answered 21/10, 2015 at 19:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.