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());
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.
Check out my blog for more of my musings upon technology and various other topics.
Top comments (0)