[javascript] JavaScript ES6 promise for loop

for (let i = 0; i < 10; i++) {
    const promise = new Promise((resolve, reject) => {
        const timeout = Math.random() * 1000;
        setTimeout(() => {
            console.log(i);
        }, timeout);
    });

    // TODO: Chain this promise to the previous one (maybe without having it running?)
}

The above will give the following random output:

6
9
4
8
5
1
7
2
3
0

The task is simple: Make sure each promise runs only after the other one (.then()).

For some reason, I couldn't find a way to do it.

I tried generator functions (yield), tried simple functions that return a promise, but at the end of the day it always comes down to the same problem: The loop is synchronous.

With async I'd simply use async.series().

How do you solve it?

This question is related to javascript es6-promise

The answer is


Based on the excellent answer by trincot, I wrote a reusable function that accepts a handler to run over each item in an array. The function itself returns a promise that allows you to wait until the loop has finished and the handler function that you pass may also return a promise.

loop(items, handler) : Promise

It took me some time to get it right, but I believe the following code will be usable in a lot of promise-looping situations.

Copy-paste ready code:

// SEE https://stackoverflow.com/a/46295049/286685
const loop = (arr, fn, busy, err, i=0) => {
  const body = (ok,er) => {
    try {const r = fn(arr[i], i, arr); r && r.then ? r.then(ok).catch(er) : ok(r)}
    catch(e) {er(e)}
  }
  const next = (ok,er) => () => loop(arr, fn, ok, er, ++i)
  const run  = (ok,er) => i < arr.length ? new Promise(body).then(next(ok,er)).catch(er) : ok()
  return busy ? run(busy,err) : new Promise(run)
}

Usage

To use it, call it with the array to loop over as the first argument and the handler function as the second. Do not pass parameters for the third, fourth and fifth arguments, they are used internally.

_x000D_
_x000D_
const loop = (arr, fn, busy, err, i=0) => {_x000D_
  const body = (ok,er) => {_x000D_
    try {const r = fn(arr[i], i, arr); r && r.then ? r.then(ok).catch(er) : ok(r)}_x000D_
    catch(e) {er(e)}_x000D_
  }_x000D_
  const next = (ok,er) => () => loop(arr, fn, ok, er, ++i)_x000D_
  const run  = (ok,er) => i < arr.length ? new Promise(body).then(next(ok,er)).catch(er) : ok()_x000D_
  return busy ? run(busy,err) : new Promise(run)_x000D_
}_x000D_
_x000D_
const items = ['one', 'two', 'three']_x000D_
_x000D_
loop(items, item => {_x000D_
  console.info(item)_x000D_
})_x000D_
.then(() => console.info('Done!'))
_x000D_
_x000D_
_x000D_

Advanced use cases

Let's look at the handler function, nested loops and error handling.

handler(current, index, all)

The handler gets passed 3 arguments. The current item, the index of the current item and the complete array being looped over. If the handler function needs to do async work, it can return a promise and the loop function will wait for the promise to resolve before starting the next iteration. You can nest loop invocations and all works as expected.

_x000D_
_x000D_
const loop = (arr, fn, busy, err, i=0) => {_x000D_
  const body = (ok,er) => {_x000D_
    try {const r = fn(arr[i], i, arr); r && r.then ? r.then(ok).catch(er) : ok(r)}_x000D_
    catch(e) {er(e)}_x000D_
  }_x000D_
  const next = (ok,er) => () => loop(arr, fn, ok, er, ++i)_x000D_
  const run  = (ok,er) => i < arr.length ? new Promise(body).then(next(ok,er)).catch(er) : ok()_x000D_
  return busy ? run(busy,err) : new Promise(run)_x000D_
}_x000D_
_x000D_
const tests = [_x000D_
  [],_x000D_
  ['one', 'two'],_x000D_
  ['A', 'B', 'C']_x000D_
]_x000D_
_x000D_
loop(tests, (test, idx, all) => new Promise((testNext, testFailed) => {_x000D_
  console.info('Performing test ' + idx)_x000D_
  return loop(test, (testCase) => {_x000D_
    console.info(testCase)_x000D_
  })_x000D_
  .then(testNext)_x000D_
  .catch(testFailed)_x000D_
}))_x000D_
.then(() => console.info('All tests done'))
_x000D_
_x000D_
_x000D_

Error handling

Many promise-looping examples I looked at break down when an exception occurs. Getting this function to do the right thing was pretty tricky, but as far as I can tell it is working now. Make sure to add a catch handler to any inner loops and invoke the rejection function when it happens. E.g.:

_x000D_
_x000D_
const loop = (arr, fn, busy, err, i=0) => {_x000D_
  const body = (ok,er) => {_x000D_
    try {const r = fn(arr[i], i, arr); r && r.then ? r.then(ok).catch(er) : ok(r)}_x000D_
    catch(e) {er(e)}_x000D_
  }_x000D_
  const next = (ok,er) => () => loop(arr, fn, ok, er, ++i)_x000D_
  const run  = (ok,er) => i < arr.length ? new Promise(body).then(next(ok,er)).catch(er) : ok()_x000D_
  return busy ? run(busy,err) : new Promise(run)_x000D_
}_x000D_
_x000D_
const tests = [_x000D_
  [],_x000D_
  ['one', 'two'],_x000D_
  ['A', 'B', 'C']_x000D_
]_x000D_
_x000D_
loop(tests, (test, idx, all) => new Promise((testNext, testFailed) => {_x000D_
  console.info('Performing test ' + idx)_x000D_
  loop(test, (testCase) => {_x000D_
    if (idx == 2) throw new Error()_x000D_
    console.info(testCase)_x000D_
  })_x000D_
  .then(testNext)_x000D_
  .catch(testFailed)  //  <--- DON'T FORGET!!_x000D_
}))_x000D_
.then(() => console.error('Oops, test should have failed'))_x000D_
.catch(e => console.info('Succesfully caught error: ', e))_x000D_
.then(() => console.info('All tests done'))
_x000D_
_x000D_
_x000D_

UPDATE: NPM package

Since writing this answer, I turned the above code in an NPM package.

for-async

Install

npm install --save for-async

Import

var forAsync = require('for-async');  // Common JS, or
import forAsync from 'for-async';

Usage (async)

var arr = ['some', 'cool', 'array'];
forAsync(arr, function(item, idx){
  return new Promise(function(resolve){
    setTimeout(function(){
      console.info(item, idx);
      // Logs 3 lines: `some 0`, `cool 1`, `array 2`
      resolve(); // <-- signals that this iteration is complete
    }, 25); // delay 25 ms to make async
  })
})

See the package readme for more details.


You can use async/await for this. I would explain more, but there's nothing really to it. It's just a regular for loop but I added the await keyword before the construction of your Promise

What I like about this is your Promise can resolve a normal value instead of having a side effect like your code (or other answers here) include. This gives you powers like in The Legend of Zelda: A Link to the Past where you can affect things in both the Light World and the Dark World – ie, you can easily work with data before/after the Promised data is available without having to resort to deeply nested functions, other unwieldy control structures, or stupid IIFEs.

// where DarkWorld is in the scary, unknown future
// where LightWorld is the world we saved from Ganondorf
LightWorld ... await DarkWorld

So here's what that will look like ...

_x000D_
_x000D_
const someProcedure = async n =>_x000D_
  {_x000D_
    for (let i = 0; i < n; i++) {_x000D_
      const t = Math.random() * 1000_x000D_
      const x = await new Promise(r => setTimeout(r, t, i))_x000D_
      console.log (i, x)_x000D_
    }_x000D_
    return 'done'_x000D_
  }_x000D_
_x000D_
someProcedure(10).then(x => console.log(x)) // => Promise_x000D_
// 0 0_x000D_
// 1 1_x000D_
// 2 2_x000D_
// 3 3_x000D_
// 4 4_x000D_
// 5 5_x000D_
// 6 6_x000D_
// 7 7_x000D_
// 8 8_x000D_
// 9 9_x000D_
// done
_x000D_
_x000D_
_x000D_

See how we don't have to deal with that bothersome .then call within our procedure? And async keyword will automatically ensure that a Promise is returned, so we can chain a .then call on the returned value. This sets us up for great success: run the sequence of n Promises, then do something important – like display a success/error message.


here's my 2 cents worth:

  • resuable function forpromise()
  • emulates a classic for loop
  • allows for early exit based on internal logic, returning a value
  • can collect an array of results passed into resolve/next/collect
  • defaults to start=0,increment=1
  • exceptions thrown inside loop are caught and passed to .catch()

_x000D_
_x000D_
    function forpromise(lo, hi, st, res, fn) {_x000D_
        if (typeof res === 'function') {_x000D_
            fn = res;_x000D_
            res = undefined;_x000D_
        }_x000D_
        if (typeof hi === 'function') {_x000D_
            fn = hi;_x000D_
            hi = lo;_x000D_
            lo = 0;_x000D_
            st = 1;_x000D_
        }_x000D_
        if (typeof st === 'function') {_x000D_
            fn = st;_x000D_
            st = 1;_x000D_
        }_x000D_
        return new Promise(function(resolve, reject) {_x000D_
_x000D_
            (function loop(i) {_x000D_
                if (i >= hi) return resolve(res);_x000D_
                const promise = new Promise(function(nxt, brk) {_x000D_
                    try {_x000D_
                        fn(i, nxt, brk);_x000D_
                    } catch (ouch) {_x000D_
                        return reject(ouch);_x000D_
                    }_x000D_
                });_x000D_
                promise._x000D_
                catch (function(brkres) {_x000D_
                    hi = lo - st;_x000D_
                    resolve(brkres)_x000D_
                }).then(function(el) {_x000D_
                    if (res) res.push(el);_x000D_
                    loop(i + st)_x000D_
                });_x000D_
            })(lo);_x000D_
_x000D_
        });_x000D_
    }_x000D_
_x000D_
_x000D_
    //no result returned, just loop from 0 thru 9_x000D_
    forpromise(0, 10, function(i, next) {_x000D_
        console.log("iterating:", i);_x000D_
        next();_x000D_
    }).then(function() {_x000D_
_x000D_
_x000D_
        console.log("test result 1", arguments);_x000D_
_x000D_
        //shortform:no result returned, just loop from 0 thru 4_x000D_
        forpromise(5, function(i, next) {_x000D_
            console.log("counting:", i);_x000D_
            next();_x000D_
        }).then(function() {_x000D_
_x000D_
            console.log("test result 2", arguments);_x000D_
_x000D_
_x000D_
_x000D_
            //collect result array, even numbers only_x000D_
            forpromise(0, 10, 2, [], function(i, collect) {_x000D_
                console.log("adding item:", i);_x000D_
                collect("result-" + i);_x000D_
            }).then(function() {_x000D_
_x000D_
                console.log("test result 3", arguments);_x000D_
_x000D_
                //collect results, even numbers, break loop early with different result_x000D_
                forpromise(0, 10, 2, [], function(i, collect, break_) {_x000D_
                    console.log("adding item:", i);_x000D_
                    if (i === 8) return break_("ending early");_x000D_
                    collect("result-" + i);_x000D_
                }).then(function() {_x000D_
_x000D_
                    console.log("test result 4", arguments);_x000D_
_x000D_
                    // collect results, but break loop on exception thrown, which we catch_x000D_
                    forpromise(0, 10, 2, [], function(i, collect, break_) {_x000D_
                        console.log("adding item:", i);_x000D_
                        if (i === 4) throw new Error("failure inside loop");_x000D_
                        collect("result-" + i);_x000D_
                    }).then(function() {_x000D_
_x000D_
                        console.log("test result 5", arguments);_x000D_
_x000D_
                    })._x000D_
                    catch (function(err) {_x000D_
_x000D_
                        console.log("caught in test 5:[Error ", err.message, "]");_x000D_
_x000D_
                    });_x000D_
_x000D_
                });_x000D_
_x000D_
            });_x000D_
_x000D_
_x000D_
        });_x000D_
_x000D_
_x000D_
_x000D_
    });
_x000D_
_x000D_
_x000D_


If you are limited to ES6, the best option is Promise all. Promise.all(array) also returns an array of promises after successfully executing all the promises in array argument. Suppose, if you want to update many student records in the database, the following code demonstrates the concept of Promise.all in such case-

let promises = students.map((student, index) => {
//where students is a db object
student.rollNo = index + 1;
student.city = 'City Name';
//Update whatever information on student you want
return student.save();
});
Promise.all(promises).then(() => {
  //All the save queries will be executed when .then is executed
  //You can do further operations here after as all update operations are completed now
});

Map is just an example method for loop. You can also use for or forin or forEach loop. So the concept is pretty simple, start the loop in which you want to do bulk async operations. Push every such async operation statement in an array declared outside the scope of that loop. After the loop completes, execute the Promise all statement with the prepared array of such queries/promises as argument.

The basic concept is that the javascript loop is synchronous whereas database call is async and we use push method in loop that is also sync. So, the problem of asynchronous behavior doesn't occur inside the loop.