JavaScript async/await in a Loop

Async/await syntax is a great technique how to deal with promises in modern JavaScript. Unfortunately it's not always easy to understand how it works which can lead to strange bugs. Let's investigate one of them.


There is a service in our system cleaning up unused resources. Those resources are grouped upon an ID. So the request looks like "remove all the resources for the ID 123".

In the service code I have found the following line:

entryKeys.forEach(async key => await removeResource(key))

Why this doesn't work? Let's analyse the code a bit. The forEach is just a stream variant of the for loop and it's definitely synchronous. So, there shouldn't be the problem even when it's a bit ineffective. Inside the forEach there is a call of the removeResource function with await. The await tells us that the function returns a promise. The await can be used only within an async function which is fulfilled because the inline (lambda) function is really marked with the async keyword (we can rewrite the same function as async function(key){ await removeReource(key) }). The point is a function declared with async returns always a promise. It means, the forEach fires several asynchronous calls but doesn't wait for them (await removeResource(key) is another asynchronous call inside that asynchronous call).

Now, when we understand why the code doesn't work, we can fix it:

for (const key of entryKeys) { await removeResource(key) } 

This works fine, the only problem is, it's synchronous and slow. The loop is waiting for an execution to finish before starting a new one even when it is possible to run them all in parallel.

Can we fix it? Sure!

await Promise.all(entryKeys.map(key => removeResource(key))) 

We have changed forEach to map, so we map an array of IDs into an array of promises. Then we pack the promises into a one big promise via Promise.all(). Now we wait for the big promise (and that's exactly what we missed before) to be completed. All the call are executed in parallel. Well done.

You can play with the different variants via the following snippet:

(async function(){
    console.log("START")
    
    // doesn't work
    //["A","B","C"].forEach(async key => await doSomethingAsync(key))
    
    await Promise.all(["D","E","F"].map(key => doSomethingAsync(key)))
    
    // synchronous
    //for (const key of ["G","H","I"]) { await doSomethingAsync(key) }   
    
    console.log("END")  
})()

function doSomethingAsync(key) {
    return new Promise(function (resolve, reject) {
        setTimeout(() => {
            console.log("RESOLVED " + key)
            resolve(key)
        }, 1000)
    })
}

What about Reduce?

How could we reduce the value from the promises? Well, of course the reduce needs the already resolved values:

// "JKL"
var result = await ["J","K","L"]
      .map(key => doSomethingAsync(key))
      .reduce(async (sum,v) => await sum + await v)

Because the whole reduce function is async we have to use await as well to retrieve the value of sum.

Happy promising!