Using map() with async/await

It happens that an array of elements is given and they need to be processed using an asynchronous function. We can use the for..of syntax or the map() method with async/await.

Let’s start with the setup, which is very simple but it serves the purpose well.

We have an array of names in index.js:

const names = ['Alice', 'Bob', 'Carol']

We also have a function in you-are-great.js. which makes these people great (because they are):

function youAreGreat(name) {
  return Promise.resolve(`You are great, ${ name }!`)
}

module.exports = youAreGreat

The task is to apply the youAreGreat function to names and log them to the console.

1. The classic

If iteration comes up as a topic, most people think of the for or for..of loops. Let’s quickly require youAreGreat.js in index.js:

// index.js
const youAreGreat = require('./you-are-great')

const names = ['Alice', 'Bob', 'Carol']

async function logInsideForOf(names) {
  for (const name of names) {
    const toLog = await youAreGreat(name)
    console.log(toLog)
  }
  console.log('Done')
}

logInsideForOf(names).catch(e => console.error(e))

// You are great, Alice!
// You are great, Bob!
// You are great, Carol!
// Done

youAreGreat returns a Promise, so we need to await it. await only works inside async functions, therefore we need to wrap the loop in a function called logInsideForOf, and invoke this function.

If we don’t wrap await youAreGreat(name) in an async function, it won’t work:

// index.js
for (const name of names) {
  const toLog = await youAreGreat(name)
  console.log(toLog)
}

// SyntaxError: await is only valid in async function

But if we don’t await it, we’ll get pending promises:

// index.js
for (const name of names) {
  const toLog = youAreGreat(name)
  console.log(toLog)
}

console.log('Done')

// Promise { <pending> }
// Promise { <pending> }
// Promise { <pending> }
// Done

None of them are good, so wrap it in an async function.

2. The functional way

for..of doesn’t suit the functional way of writing code. Let’s make it functional then.

2.1. forEach()

forEach just seems to do the job. It applies a callback function on each element in the array (in our case, it’s names), and this is exactly what we want.

// index.js

function logInsideForEach(names) {
  names.forEach(async (name) => {
    const toLog = await youAreGreat(name)
    console.log(toLog)
  })
  console.log('All logged')
}

logInsideForEach(names)

// All logged
// You are great, Alice!
// You are great, Bob!
// You are great, Carol!

Oops. It’s not working as expected. Alice and her friends are still great, but they are only great after the line All logged has been executed.

forEach processes the greatness synchronously, and it doesn’t wait for the promise to resolve, which is an asynchronous process. Although it applies the async callback function on each name, it all happens in the next cycle of the event loop.

2.2. map() with async/await

We can use map with Promise.all.

Promise.all returns a promise and accepts an iterable object as an argument. Arrays are iterables, and because map returns an array, we are good to go.

We can write the following syntax:

// index.js

async function logInsideMap(names) {
  const [Alice, Bob, Carol] = await Promise.all(names.map(youAreGreat))
  console.log(Alice)
  console.log(Bob)
  console.log(Carol)
  console.log('All logged')
}

logInsideMap(names).catch(e => console.error(e))

// You are great, Alice!
// You are great, Bob!
// You are great, Carol!
// All logged

names.map returns an array of promises, because the youAreGreat itself returns a promise. For each name in names, we’ll return youAreGreat. The order of the names is kept, and we can use Array destructuring to access the values.

The above code could have been written differently, it all depends on the situation and the function we want to invoke on each element of the array.

If we don’t want to use destructuring, we can do something like this:

// index.js

async function logInsideMap() {
  await Promise.all(names.map(async (name) => {
    const toLog = await youAreGreat(name)
    console.log(toLog)
  }))
  console.log('All logged')
}

logInsideMap(names).catch(e => console.error(e))

// You are great, Alice!
// You are great, Bob!
// You are great, Carol!
// All logged

Because nothing is returned from map, we just await youAreGreat to resolve with each name, which means that the array map returns will contain one single promise. If we want to log the All logged message, we need to await Promise.all as well.

3. Conclusion

If we need to apply asynchronous code on multiple elements in an array, we can use the for..of syntax or in the functional way, the map method with Promise.all.

Thanks for reading and see you next time.