Using Async / Await with Callbacks



I recently ran into the problem of getting a promise interface working alongside a callback interface. Specifically I had an array of promises and wanted to iterate over the values with JavaScript's "forEach" method. As we will see this is problematic, however a solution is readily available. It took me a while to understand what was going on, but hopefully you can skip the pain that I had as I will explain it all here.
Disclaimer: The code in this blog post was reduced to highlight the source of the problem. This is not the best way to implement this functionality in JavaScript. It is instead meant to make the problem and resolution clear.

A promise-less starting point

I started with an array of values. I performed some logic on this array combining the values into one and then returned the result.

function getMovieNames() {
  let displayToUser = '';
  let movieNames = [
    "Lord of the Rings",
    "Band of Brothers",
    "Interstellar"
  ];

  movieNames.forEach(movieName =>
    displayToUser += movieName + ' ');

  return displayToUser;
}

alert(getMovieNames());

This is simple enough: it alerts a combined string of movie names. No problems so far.

A promise filled update

For reasons external to the problem at hand, I had to take the data that I was receiving and fetch other data with it. Now instead of having an array of values we iterated over these values and retrieved asynchronous data. Can you guess what get's returned?

function getReleaseDates() {
  let displayToUser = '';
  let movieNames = [
    "Lord of the Rings",
    "Band of Brothers",
    "Interstellar"
  ];

  movieNames.forEach(movieName =>
    displayToUser += fetchReleaseDate(movieName));

  return displayToUser;
}

alert(getReleaseDates());

An empty string! Of course you can't know without seeing the implementation of "fetchReleaseDate", but if we assume it is making an HTTP request or taking some similarly asynchronous action, then "getReleaseDates" is returned before the "forEach" callback is ever executed.

Await to the rescue

If we need to await for the "fetchReleaseDate" method, it seems like the await keyword should work for that. Right? In order to await for "fetchReleaseDate" the function it is in needs to be labeled as async. In this case that is the callback that we are giving the "forEach" method.

async function getReleaseDates() {
  let displayToUser = '';
  let movieNames = [
    "Lord of the Rings",
    "Band of Brothers",
    "Interstellar"
  ];

  movieNames.forEach(async movieName =>
    displayToUser += await fetchReleaseDate(movieName));

  return displayToUser;
}

alert(await getReleaseDates());

Not so fast! Even if we await for the "fetchReleaseDate" method, the "forEach" method does not await for our callback. And so the result is no different than before.

For of to the rescue

If the callback not being awaited on is the problem, the solution is to remove the callback. To do this let's change out the use of "forEach" with a "for...of" block as shown below.

async function getReleaseDates() {
  let displayToUser = '';
  let movieNames = [
    "Lord of the Rings",
    "Band of Brothers",
    "Interstellar"
  ];

  for (movieName of movieNames) {
    displayToUser += await fetchReleaseDate(movieName);
  }

  return displayToUser;
}

alert(await getReleaseDates());
// SUCCESS! Now that our await is not dependant upon a callback method,
// this code will print the release dates.

Finally! It works again. While this may be obvious to some, others like me who are addicted to the "forEach" and "map" style of using arrays may not see this gotchya with using async / await along side callbacks.
Parting tip: If you ever declare a callback function as async in order to use the await keyword inside, ask yourself first: "Is the function I am calling going to await on my callback?" If the answer is no, then you will need to refactor your use of the callback function.

Popular Posts