JavaScript ES6 promise for loop

JavascriptEs6 Promise

Javascript Problem Overview


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?

Javascript Solutions


Solution 1 - Javascript

As you already hinted in your question, your code creates all promises synchronously. Instead they should only be created at the time the preceding one resolves.

Secondly, each promise that is created with new Promise needs to be resolved with a call to resolve (or reject). This should be done when the timer expires. That will trigger any then callback you would have on that promise. And such a then callback (or await) is a necessity in order to implement the chain.

With those ingredients, there are several ways to perform this asynchronous chaining:

  1. With a for loop that starts with an immediately resolving promise

  2. With Array#reduce that starts with an immediately resolving promise

  3. With a function that passes itself as resolution callback

  4. With ECMAScript2017's async / await syntax

  5. With ECMAScript2020's for await...of syntax

But let me first introduce a very useful, generic function.

Promisfying setTimeout

Using setTimeout is fine, but we actually need a promise that resolves when the timer expires. So let's create such a function: this is called promisifying a function, in this case we will promisify setTimeout. It will improve the readability of the code, and can be used for all of the above options:

const delay = ms => new Promise(resolve => setTimeout(resolve, ms));

See a snippet and comments for each of the options below.

1. With for

You can use a for loop, but you must make sure it doesn't create all promises synchronously. Instead you create an initial immediately resolving promise, and then chain new promises as the previous ones resolve:

const delay = ms => new Promise(resolve => setTimeout(resolve, ms));

for (let i = 0, p = Promise.resolve(); i < 10; i++) {
    p = p.then(() => delay(Math.random() * 1000))
         .then(() => console.log(i));
}

So this code creates one long chain of then calls. The variable p only serves to not lose track of that chain, and allow a next iteration of the loop to continue on the same chain. The callbacks will start executing after the synchronous loop has completed.

It is important that the then-callback returns the promise that delay() creates: this will ensure the asynchronous chaining.

2. With reduce

This is just a more functional approach to the previous strategy. You create an array with the same length as the chain you want to execute, and start out with an immediately resolving promise:

const delay = ms => new Promise(resolve => setTimeout(resolve, ms));

[...Array(10)].reduce( (p, _, i) => 
    p.then(() => delay(Math.random() * 1000))
     .then(() => console.log(i))
, Promise.resolve() );

This is probably more useful when you actually have an array with data to be used in the promises.

3. With a function passing itself as resolution-callback

Here we create a function and call it immediately. It creates the first promise synchronously. When it resolves, the function is called again:

const delay = ms => new Promise(resolve => setTimeout(resolve, ms));

(function loop(i) {
    if (i >= 10) return; // all done
    delay(Math.random() * 1000).then(() => {
        console.log(i);
        loop(i+1);
    });
})(0);

This creates a function named loop, and at the very end of the code you can see it gets called immediately with argument 0. This is the counter, and the i argument. The function will create a new promise if that counter is still below 10, otherwise the chaining stops.

When delay() resolves, it will trigger the then callback which will call the function again.

4. With async/await

Modern JS engines support this syntax:

const delay = ms => new Promise(resolve => setTimeout(resolve, ms));

(async function loop() {
    for (let i = 0; i < 10; i++) {
        await delay(Math.random() * 1000);
        console.log(i);
    }
})();

It may look strange, as it seems like the promises are created synchronously, but in reality the async function returns when it executes the first await. Every time an awaited promise resolves, the function's running context is restored, and proceeds after the await, until it encounters the next one, and so it continues until the loop finishes.

5. With for await...of

With EcmaScript 2020, the for await...of found its way to modern JavaScript engines. Although it does not really reduce code in this case, it allows to isolate the definition of the random interval chain from the actual iteration of it:

const delay = ms => new Promise(resolve => setTimeout(resolve, ms));

async function * randomDelays(count, max) {
    for (let i = 0; i < count; i++) yield delay(Math.random() * max).then(() => i);
}

(async function loop() {
    for await (let i of randomDelays(10, 1000)) console.log(i);
})();

Solution 2 - Javascript

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 ...

async function someProcedure (n) {
  for (let i = 0; i < n; i++) {
    const t = Math.random() * 1000
    const x = await new Promise(r => setTimeout(r, t, i))
    console.log (i, x)
  }
  return 'done'
}

someProcedure(10)
  .then(console.log)
  .catch(console.error)

0 0
1 1
2 2
3 3
4 4
5 5
6 6
7 7
8 8
9 9
done

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.

Solution 3 - Javascript

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.

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)
}

const items = ['one', 'two', 'three']

loop(items, item => {
  console.info(item)
})
.then(() => console.info('Done!'))

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.

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)
}

const tests = [  [],
  ['one', 'two'],
  ['A', 'B', 'C']
]

loop(tests, (test, idx, all) => new Promise((testNext, testFailed) => {
  console.info('Performing test ' + idx)
  return loop(test, (testCase) => {
    console.info(testCase)
  })
  .then(testNext)
  .catch(testFailed)
}))
.then(() => console.info('All tests done'))

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.:

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)
}

const tests = [  [],
  ['one', 'two'],
  ['A', 'B', 'C']
]

loop(tests, (test, idx, all) => new Promise((testNext, testFailed) => {
  console.info('Performing test ' + idx)
  loop(test, (testCase) => {
    if (idx == 2) throw new Error()
    console.info(testCase)
  })
  .then(testNext)
  .catch(testFailed)  //  <--- DON'T FORGET!!
}))
.then(() => console.error('Oops, test should have failed'))
.catch(e => console.info('Succesfully caught error: ', e))
.then(() => console.info('All tests done'))

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.

Solution 4 - Javascript

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.

Solution 5 - Javascript

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()

    function forpromise(lo, hi, st, res, fn) {
        if (typeof res === 'function') {
            fn = res;
            res = undefined;
        }
        if (typeof hi === 'function') {
            fn = hi;
            hi = lo;
            lo = 0;
            st = 1;
        }
        if (typeof st === 'function') {
            fn = st;
            st = 1;
        }
        return new Promise(function(resolve, reject) {

            (function loop(i) {
                if (i >= hi) return resolve(res);
                const promise = new Promise(function(nxt, brk) {
                    try {
                        fn(i, nxt, brk);
                    } catch (ouch) {
                        return reject(ouch);
                    }
                });
                promise.
                catch (function(brkres) {
                    hi = lo - st;
                    resolve(brkres)
                }).then(function(el) {
                    if (res) res.push(el);
                    loop(i + st)
                });
            })(lo);

        });
    }


    //no result returned, just loop from 0 thru 9
    forpromise(0, 10, function(i, next) {
        console.log("iterating:", i);
        next();
    }).then(function() {


        console.log("test result 1", arguments);

        //shortform:no result returned, just loop from 0 thru 4
        forpromise(5, function(i, next) {
            console.log("counting:", i);
            next();
        }).then(function() {

            console.log("test result 2", arguments);



            //collect result array, even numbers only
            forpromise(0, 10, 2, [], function(i, collect) {
                console.log("adding item:", i);
                collect("result-" + i);
            }).then(function() {

                console.log("test result 3", arguments);

                //collect results, even numbers, break loop early with different result
                forpromise(0, 10, 2, [], function(i, collect, break_) {
                    console.log("adding item:", i);
                    if (i === 8) return break_("ending early");
                    collect("result-" + i);
                }).then(function() {

                    console.log("test result 4", arguments);

                    // collect results, but break loop on exception thrown, which we catch
                    forpromise(0, 10, 2, [], function(i, collect, break_) {
                        console.log("adding item:", i);
                        if (i === 4) throw new Error("failure inside loop");
                        collect("result-" + i);
                    }).then(function() {

                        console.log("test result 5", arguments);

                    }).
                    catch (function(err) {

                        console.log("caught in test 5:[Error ", err.message, "]");

                    });

                });

            });


        });



    });

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionPoniView Question on Stackoverflow
Solution 1 - JavascripttrincotView Answer on Stackoverflow
Solution 2 - JavascriptMulanView Answer on Stackoverflow
Solution 3 - JavascriptStijn de WittView Answer on Stackoverflow
Solution 4 - JavascriptSrk95View Answer on Stackoverflow
Solution 5 - JavascriptcestmoiView Answer on Stackoverflow