Best way to call an async function within JavaScript map

What is the best way to call an asynchronous function within a map in JavaScript?

I’m mapping over an array, and for one of the return values in the new object, I need to make an asynchronous call. Here’s my current implementation:

var firebaseData = teachers.map(function(teacher) {
  return {
    name: teacher.title,
    description: teacher.body_html,
    image: urlToBase64(teacher.summary_html.match(/src="(.*?)"/)[1]),
    city: metafieldTeacherData[teacher.id].city,
    country: metafieldTeacherData[teacher.id].country,
    state: metafieldTeacherData[teacher.id].state,
    studioName: metafieldTeacherData[teacher.id].studioName,
    studioURL: metafieldTeacherData[teacher.id].studioURL
  };
});

The asynchronous function, urlToBase64, looks like this:
function urlToBase64(url) {
  request.get(url, function (error, response, body) {
    if (!error && response.statusCode == 200) {
      return "data:" + response.headers["content-type"] + ";base64," + new Buffer(body).toString('base64');
    }
  });
}

I’m uncertain about the best approach to implement this. Should I use promises, nested callbacks, or ES6/ES7 features, possibly transpiling with Babel? What’s the current best way to handle this scenario using javascript async map?

I have a good amount of experience working with JavaScript async/await, and for this situation, using Promise.all with async/await is a solid approach. This method ensures that you’ll be able to handle each asynchronous operation cleanly while leveraging the full power of JavaScript’s modern async handling.

Here’s how it works:

async function fetchFirebaseData(teachers) {
  const firebaseData = await Promise.all(teachers.map(async (teacher) => {
    const image = await urlToBase64(teacher.summary_html.match(/src="(.*?)"/)[1]);
    return {
      name: teacher.title,
      description: teacher.body_html,
      image: image,
      city: metafieldTeacherData[teacher.id].city,
      country: metafieldTeacherData[teacher.id].country,
      state: metafieldTeacherData[teacher.id].state,
      studioName: metafieldTeacherData[teacher.id].studioName,
      studioURL: metafieldTeacherData[teacher.id].studioURL
    };
  }));
  return firebaseData;
}

Using Promise.all in this way allows the asynchronous calls to run in parallel rather than sequentially, enhancing performance. This approach is efficient and readable and takes advantage of async/await for an improved handling of async behavior in JavaScript. It’s a clean way to handle javascript async map scenarios, especially for large arrays.

If you prefer avoiding async/await, you can use Promises directly with map and Promise.all. This method might feel more familiar if you’re working with older codebases that use Promises without async/await.

Here’s an alternative approach using Promises:

function urlToBase64(url) {
  return new Promise((resolve, reject) => {
    request.get(url, (error, response, body) => {
      if (!error && response.statusCode === 200) {
        resolve("data:" + response.headers["content-type"] + ";base64," + Buffer.from(body).toString('base64'));
      } else {
        reject(error);
      }
    });
  });
}

function fetchFirebaseData(teachers) {
  return Promise.all(teachers.map(teacher => {
    return urlToBase64(teacher.summary_html.match(/src="(.*?)"/)[1])
      .then(image => ({
        name: teacher.title,
        description: teacher.body_html,
        image: image,
        city: metafieldTeacherData[teacher.id].city,
        country: metafieldTeacherData[teacher.id].country,
        state: metafieldTeacherData[teacher.id].state,
        studioName: metafieldTeacherData[teacher.id].studioName,
        studioURL: metafieldTeacherData[teacher.id].studioURL
      }));
  }));
}

Using this method, you still achieve asynchronous handling within the map by returning Promises for each item. Promise.all ensures all Promises resolve before returning the final array. This solution is great when javascript async map calls need to work with Promises directly, without relying on async/await.

For those who prefer working with side effects or are unable to use map in this context, using forEach is an alternative. Although forEach doesn’t return an array directly, it does allow you to control async flow. This approach is practical if you’re handling each item’s asynchronous call independently.

Here’s how to use forEach with async/await for this purpose:

async function fetchFirebaseData(teachers) {
  const firebaseData = [];
  for (const teacher of teachers) {
    const image = await urlToBase64(teacher.summary_html.match(/src="(.*?)"/)[1]);
    firebaseData.push({
      name: teacher.title,
      description: teacher.body_html,
      image: image,
      city: metafieldTeacherData[teacher.id].city,
      country: metafieldTeacherData[teacher.id].country,
      state: metafieldTeacherData[teacher.id].state,
      studioName: metafieldTeacherData[teacher.id].studioName,
      studioURL: metafieldTeacherData[teacher.id].studioURL
    });
  }
  return firebaseData;
}

This method processes each item sequentially, which can be beneficial if you need to handle them one by one, as opposed to running them in parallel. Although not as performant for larger datasets, it’s sometimes more readable. For scenarios where you need controlled async handling within a javascript async map setup, using forEach can be a simpler solution.