[Javascript] forEach() doesn't wait for async/await

May 13, 2021

programming
article cover

“await” doesnt’ work in forEach()

Let’s say there is a situation that we have to make multiple API calls,

const delay = () => {
  return new Promise(resolve => setTimeout(resolve, Math.random() * 1500))
}

// Fake API
const API = (ep) => {
  return delay().then(() => console.log(`API [${ep}] is called.`))
}

and make an API call respectively in forEach().

const asyncInForEach = () => {
  console.log('start')
  
  const endpoints = ['endpoint1', 'endpoint2', 'endpoint3', 'endpoint4']
    
  endpoints.forEach(async (ep) => {
    await API(ep)
  })
  
  console.log('end')
}

asyncInForEach()

Can you tell what the result would be? Maybe you would expect a result like the following.

start
API [1] is called.
API [2] is called.
API [3] is called.
API [4] is called.
end

But it’s actually not.

screen shot

forEach() finished its process without waiting for the return from API even though we did await.

This occurs because forEach() doesn’t support async/await. So await inside forEach() will never work as we expect.

Solutions

1. Use for loop

const asyncInForLoop = async () => {
  console.log('start')
  
  const endpoints = ['endpoint1', 'endpoint2', 'endpoint3', 'endpoint4']
  
  for (let i = 0; i < 4; i++) {
    await API(endpoints[i])
  }
  
  console.log('end')
}

asyncInForLoop()

Since for loop does support async/await, it works fine as we expect.
It’s simple and easy-to-use but Promise.all() which I’m gonna explain next is more efficient.

screen shot

2. Use Promise.all()

Promise.all() takes an iterable of promises as an input, and returns a single Promise.
This process will end when it meets one of the following conditions.

  • All of the input’s promises have resolved
  • Any of the input promises have rejected

And Promise.all() also enable us to handle API calls parallelly.

const asyncWithPromiseAll = async () => {
  console.log('start')
  
  const endpoints = ['endpoint1', 'endpoint2', 'endpoint3', 'endpoint4']
  
  await Promise.all(
  	endpoints.map((ep) => {
      return API(ep)
    })
  )
  
  console.log('end')
}

asyncWithPromiseAll()

screen shot

You can see that the order of returns from API doesn’t matter to the order of making calls.

Note about using Promise.all()

As I mentioned, Promise.all() will end when any of the input Promises have rejected.
This is maybe preferable behaviour in some situation but better to be aware of that and be capable to handle it.

Let’s make the sample code a bit more realistic.

Add a fail case to API,

const delay = () => {
  return new Promise(resolve => setTimeout(resolve, Math.random() * 1500))
}

const API = (ep) => {
+  if (ep === 'fail') {
+    return new Promise((_, reject) => {
+      reject(new Error('error on api call'))
+    })
+  }
  
  return delay().then(() => `return from [${ep}]`)
}

and get the return values as an array.

const fetchParallel = async () => {
  const endpoints = [
    'endpoint1', 
    'endpoint2', 
    'endpoint3', 
    'endpoint4',
    'endpoint5',
  ]
  
  const result = await Promise.all(
    endpoints.map(ep => API(ep))
  )
    .catch(error => error.message)
  
  console.log(result)
}

fetchParallel()

It works as we expect.

// return

[
  "return from [endpoint1]", 
  "return from [endpoint2]", 
  "return from [endpoint3]", 
  "return from [endpoint4]", 
  "return from [endpoint5]"
]

Then now let’s make an invalid call on purpose.

const fetchParallel = async () => {
  const endpoints = [
    'endpoint1', 
    'endpoint2', 
    'endpoint3', 
    'endpoint4',
    'endpoint5',
+   'fail',  // added this
  ]
  
  const result = await Promise.all(
    endpoints.map(ep => API(ep))
  )
    .catch(error => error.message)
  
  console.log(result)
}

fetchParallel()
// return 

"error on api call"

We can’t get any of return values because of one single rejection.

To prevent this happens, add catch() to each Promise.

const fetchParallel = async () => {
  const endpoints = [
    'endpoint1', 
    'endpoint2', 
    'endpoint3', 
    'endpoint4',
    'endpoint5',
    'fail',
  ]
  
+ const errorHandler = (e) => {
+   // do something
+   return null
+ }
  
  const result = await Promise.all(
+   endpoints.map(ep => API(ep).catch(errorHandler)) 
  )
    .catch(error => error.message)
  
  console.log(result)
}

fetchParallel()

*Be careful about the difference between catch() for Promise.all() and catch() for each Promise.

// return 
[
  "return from [endpoint1]", 
  "return from [endpoint2]", 
  "return from [endpoint3]", 
  "return from [endpoint4]", 
  "return from [endpoint5]",
]

Perfect! We made API calls parallelly.


Profile picture

Photographer / Web engineer working in Berlin.
A Japanese living with a Slovak wife and two German cats.