How to use async/await in forEach method

How to use async/await in forEach method

Async/await syntax is awesome. It allows writing asynchronous code that looks just as synchronous. No more callbacks. No more nested code. But it seems that it doesn't always work. Let me show you what I mean.

Sequential task processing

Let's assume we have some complex tasks which we want to process one by one. The execution of the second one depends on finishing the first one, the execution of the third one depends on the execution of the second one and so on. We can model this situation with the following code.

function wait(timeout = 0) {
    return new Promise((resolve) => setTimeout(resolve, timeout));
}

const tasks = [
    () => wait(100).then(() => console.log("Task 1 finished at", getMillisecondsElapsed())),
    () => wait(100).then(() => console.log("Task 2 finished at", getMillisecondsElapsed())),
    () => wait(100).then(() => console.log("Task 3 finished at", getMillisecondsElapsed())),
];

Here we have three asynchronous tasks, each of them waits for 100 ms and logs when it finishes. The getMillisecondsElapsed function is here just for demonstration purposes and returns the number of milliseconds from the script start.

Let's implement our sequential processing. The idea here might be to loop over the items in the array, awaiting the execution of each function before we proceed to the next iteration.

async function run(tasks = []) {
    for (const task of tasks) {
        await task();
    }
    console.log("All tasks finished at", getMillisecondsElapsed());
}

run(tasks)

With this, we get the following output in the console.

Task 1 finished at 104
Task 2 finished at 215
Task 3 finished at 319
All tasks finished at 321

As we can see, each task finishes roughly 100 ms after the previous one, as expected. All works fine.

Using inbuilt array method

There is an inbuilt array method that allows looping over the elements of an array - Array.prototype.forEach. Since all our tasks are defined in an array, why don't we try it?

function run(tasks = []) {
    tasks.forEach(async (task) => {
        await task();
    });
    console.log("All tasks finished at", getMillisecondsElapsed());
}

run(tasks)

We get the following result in the console.

All tasks finished at 2
Task 1 finished at 104
Task 2 finished at 106
Task 3 finished at 107

The result might be surprising. Isn't forEach just the equivalent of a regular for loop? Why do we have a completely different result? Why did my function finish before executing the tasks? Let's unpack what's happening there.

First, let's focus on the tasks themselves. Why doesn't async/await work in a forEach method? Well, it does in fact work, although not in a way you might expect. As you see in the console, each task has finished after 100 ms, so the execution of every task function is awaited. To better demonstrate that, let's add 100 ms of waiting before each task.

function run(tasks = []) {
    tasks.forEach(async (task) => {
        await wait(100);
        await task();
    });
    console.log("All tasks finished at", getMillisecondsElapsed());
}

run(tasks)

We get the following result in the console.

All tasks finished at 8
Task 1 finished at 229
Task 2 finished at 231
Task 3 finished at 232

Now it takes about 200 ms for each task to finish. This shows that await works correctly within the function callback passed to forEach. To understand now why our tasks are executed nearly in parallel rather than in sequence, let's look at what could be a simplified implementation of Array.prototype.forEach (or a complete polyfill implementation for this method).

Array.prototype.forEach = function (callback) {
    for (let i = 0; i < this.length; i++) {
        callback(this[i], i, this);
    }
};

The code here might look familiar to what we did in the very beginning. We have a loop in which we execute some function in every iteration. However, there's one major difference. There's nothing that awaits the execution of the callback. We just went through the array, executing the callback function and immediately proceeding to the next element of the array, without waiting for whatever is returned from callback to resolve.

The forEach method is synchronous. Passing an asynchronous callback does not make the method itself asynchronous.

Making forEach asynchronous

Fine, but what can we do to make it work? Let's try to reimplement this method, for demonstration purposes, using so-called monkey patching.

Array.prototype.forEach = async function (callback) {
    for (let i = 0; i < this.length; i++) {
        await callback(this[i], i, this);
    }
};

Disclaimer: monkey patching is considered a bad practice, so unless you have very strong reasons to use it in your application, stay away from it, since it might cause confusion and a lot of unexpected problems.

I just added the await keyword before callback, so that we wait for the promise it returns to resolve, before going to the next iteration. Now let's re-run our initial example and see what we've got.

All tasks finished at 9
Task 1 finished at 112
Task 2 finished at 218
Task 3 finished at 327

Much better. The tasks now execute one after another, the same way as when using a regular loop. There's still one more thing - first, we get the information that all tasks have finished, and only then logs from the tasks appear. The reason for this is that we made forEach asynchronous - so to make sure that we log after this function finishes, we have to await its result as well.

async function run(tasks = []) {
    await tasks.forEach(async (task) => {
        await task();
    });
    console.log("All tasks finished at", getMillisecondsElapsed());
}

run(tasks)

Finally, we get the following.

Task 1 finished at 120
Task 2 finished at 233
Task 3 finished at 338
All tasks finished at 340

Congratulations! We've successfully implemented sequential processing using a forEach method.

Now the question is - should we do this in working applications? The answer here is - probably not. Making such a change would require adjusting any code that already uses this method to properly handle the returned promise. It would also surprise anyone who's familiar with how this method works currently.

A better alternative would be creating a custom asyncForEach method, using Array.prototype.reduce to implement this mechanism or sticking with a plain old for loop.

Conclusion

Using async/await syntax can simplify a lot in the code. But sometimes, to understand what's going on, it's worth remembering that this is only a syntactic sugar over JavaScript Promise API, and all of the characteristics of that mechanism still apply. Use it consciously!

Further reading and references