JavaScript ES6 promise for loop

Poni picture Poni · Oct 30, 2016 · Viewed 138.5k times · Source
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?

Answer

trincot picture trincot · Oct 30, 2016

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

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

1. With for

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

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

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:

[...Array(10)].reduce( (p, _, i) => 
    p.then(_ => new Promise(resolve =>
        setTimeout(function () {
            console.log(i);
            resolve();
        }, Math.random() * 1000)
    ))
, 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:

(function loop(i) {
    if (i < 10) new Promise((resolve, reject) => {
        setTimeout( () => {
            console.log(i);
            resolve();
        }, Math.random() * 1000);
    }).then(loop.bind(null, 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.

The call to resolve() will trigger the then callback which will call the function again. loop.bind(null, i+1) is just a different way of saying _ => loop(i+1).

4. With async/await

Modern JS engines support this syntax:

(async function loop() {
    for (let i = 0; i < 10; i++) {
        await new Promise(resolve => setTimeout(resolve, Math.random() * 1000));
        console.log(i);
    }
})();

It may look strange, as it seems like the new Promise() calls are executed 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.

As it may be a common thing to return a promise based on a timeout, you could create a separate function for generating such a promise. This is called promisifying a function, in this case setTimeout. It may improve the readability of the code:

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

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