Don’t Make This Async/Await Oopsie!

Suppose we need to perform some IO on items of an array, like fetching the owners of cats from the cats’ IDs using some API.
const catIDs = [132, 345, 243, 121, 423];

Let’s say we decide to use our newly acquired async/await skills to do the job. Async/await gets rid of the need for callbacks (in most cases), making asynchronous code look similar to synchronous code. But if we forget that we’re still just dealing with asynchronous code, we might make a mistake that defeats the entire purpose of having concurrency.
We might be tempted to do something like this:
async function fetchOwners(catIDs) {
const owners = [];
for (const id of catIDs) {
const cat = await fetchCat(id);
const owner = await fetchOwner(cat.ownerID);
owners.push(owner);
}
return owners;
}

What Was Our Oopsie? 🤷‍♂️

We run the code and all seems to be working alright. But there’s a glaring problem in how we’ve used async/await. The problem is that we used await within a for-loop. This problem is actually indicative of a common code smell, which is the use of .push within a for-loop instead of using .map to perform an array transformation (we’ll get to this later).
Because of the await within the for-loop, the synchronous-looking fetchOwners function is performing a fetch for the cats sequentially (kind of), instead of in parallel. The code awaits for the owner of one cat before moving on to the next for-loop iteration to fetch the owner of the next cat. Fetching the owner of one cat isn’t dependent on any other cat, but we’re acting like it is. So we’re completely missing out on the ability to fetch the owners in parallel (oopsie! 🤷‍♂️).
I mentioned “kind of" sequentially because those sequential for-loop iterations are interleaved with other procedures (through the Event Loop), since the for-loop iterations await within an async function.

What We Should Be Doing 😎

We shouldn’t await within a for-loop. In fact, this problem would be better off solved without a for-loop even if the code were synchronous. A .map is the appropriate solution, because the problem we’re dealing with is an array transformation, from an array of cat IDs to an array of owners.
This is how we’d do it using .map if the code were synchronous.
// catIDs -> owners
const owners = catIDs.map(id => {
const cat = fetchCatSync(id);
const owner = fetchOwnerSync(cat.ownerID);
return owner;
});

Since the code is actually asynchronous, we first need to transform an array of cat IDs to an array of promises (promises to the cats’ owners) and then unpack that array of promises using await to get the owners. This code doesn’t handle rejected promises for the sake of simplicity.
// catIDs -> ownerPromises -> owners
async function fetchOwners(catIDs) {
const ownerPromises = catIDs.map(id => {
return fetchCat(id)
.then(cat => fetchOwner(cat.ownerID));
});
const owners = await Promise.all(ownerPromises);
return owners;
}

To further flex our async/await skills, we could pass an async callback to the map method and await all intermediate responses (here, fetching a cat to get it’s owner’s ID) within that callback.
async function fetchOwners(catIDs) {
const ownerPromises = catIDs.map(async id => {
const cat = await fetchCat(id);
const owner = await fetchOwner(cat.ownerID);
return owner;
});
const owners = await Promise.all(ownerPromises);
return owners;
}

We’re now fetching the owners in parallel like we intended to 🙌! Oopsie undone!
This was my very first post. Not just my first on DEV, but my first, ever. Hope you liked it.

Link: https://dev.to/mebble/dont-make-this-asyncawait-mistake-1eao