Ask any JavaScript developer, and they will all agree:
-
async/await
Ā is frigging insane! It is that good. -
Except when you decide to use it inside aĀ
forEach()
. Then it can get real ugly real fast.
Your search for any query around usingĀ async/await
Ā withĀ forEach()
Ā in JavaScript - one of the first results would be a StackOverflow question.
Source Search query: https://www.google.com/search?q=using+Async%2FAwait+with+forEach%28%29+in+JavaScript
There are countless developers who have fallen prey to this beast and have struggled to sort out the mess. And that is exactly what this guide will help you overcome.
Once you have completed this guide, never again will you need to come across Google search results that look like this:
Source: https://www.youtube.com/watch?v=4lqJBBEpjRE
So, without further delay, letās brew a fresh cup of coffee and get started!
What issue does async/await pose in a JavaScript code with forEach()
The problem?
Let us look at some code to understand the problem:
const subscribedUsers = await this.fetchYesterdaysSubscriptions();
await subscribedUsers.forEach(async (user) => {
await sendPromoCode(user);
});
await sendEmailToAdmin("All new subscribers have been sent the promo code");
The intent of the code is fairly straight-forward:
subscribedUsers
Ā waits for the result of the function that fetches my new subscribers from yesterday.- To eachĀ
user
Ā in theĀsubscribedUsers
Ā array, I am sending a promotional code for my product. - Once each user has been sent the promo code, the system sends a mail to me informing me that the action has beenĀ successfullyĀ completed.
This is what you would expect this code block to do.
It is also a fairly common use case. You would be using codes like this for a bunch of different desired outcomes. So, chances are, you have already run into such problems earlier.
But there are multiple things wrong with this code, and this is what is actually happening behind the scenes:
-
There is no error handling for the promo code sending process. So, if one of the iteration throws an error, the error wouldnāt be caught. Not good! (But, we will ignore the error-handling for the time being ā _one problem at a time, my friend. One problem at a time.)
-
You add a
console.log
with a currentTime directive in there, and you will find that each iteration is happening in parallel. But, the way the code is written, you would have expected them to happen in series ā one after another. -
The loop finishes iterating before any of the promo code emails have been sent. Yes, the process of sending the emails has started, but the code is not waiting for the process to resolve before moving on. Which is not how you wanted the code to flow.
-
The admin is sent the confirmation email even though none of the promo codes have been sent yet. Maybe some of them will fail, maybe they all will. The admin will never know because he was sent the mail independent of how the promise resolved itself.
The reason behind this problem:
To put things in an extremely simplistic manner,Ā [Array.prototype.forEach](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach)
Ā was never designed for asynchronous code.
It was not suitable for promises, even when there wereĀ [Promise.then()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then)
Ā in all the code you would see around you. And now, in the world ofĀ async/await
, it is still not suitable forĀ async/await
.
So, how do we solve it?
There are a few different ways of solving this issue.
The most basic and simplest answer is to not useĀ forEach()
Ā with your async/await.
After all, ifĀ forEach()
Ā is not designed for asynchronous operations, why expect it to do something it isn't made to do naturally.
For....of
Ā will work perfectly for any usecase you may have where you want to useĀ forEach
.
But, what if you preferred theĀ forEach
Ā approach. Okay, Mr. Picky! We will have a solution for that as well. But, fair warning - you would be better suited with the other approaches!
However, lets digress a bit. Hit the breaks on solving the problem of getting your async/await to work perfectly in a forEach loop (using both methods ---Ā for...of
Ā as well asĀ forEach
).
Let us take a moment to get our basics right, and briefly understand:
-
What exactly is Async/Await in JavaScript?
-
Why is it important for JavaScript developers to be familiar with async/await?
-
How do you use async/await in your JavaScript code?
-
Should you use async/await over Promises in your code? Which is better?
Letās get on with it then!
What is Async/Await in JavaScript and Why Should You Care?
What is async/await?
Async/await allows your asynchronous JavaScript code to execute without blocking the main thread.
The async keyword specifies the function as an asynchronous operation. This makes sure that the function will always return a promise.
Source: https://javascript.info/async-await
In this function,Ā return 1
Ā is essentially the same as returnĀ Promise.resolve(1)
The await keyword specifies which function should be awaited. Please note: the await keyword works (or can be used) only inside of an async function.
Please note: With the latest updates, most modern browsers will let you use top-level awaits, if you are working in a module. But for now, letās keep things simple.
All the await keyword does is pause the code execution wherever it is used until the promise it is used with is resolved.
This codeblock from javascript.info will clear things up:
So, any code fragment that comes after the await line waits its turn and is executed only when the promise has been resolved.
Let me reiterate. The keywordĀ await
Ā will literally suspend any further execution of the function until the promise has been resolved. It is then (and only then), that the function resumes, and when it does, it resumes with the result we received from the promise.
Why should you care?
-
It is a more elegant way of writing code.
-
It is more efficient.
-
It is less resource-heavy. Since the function execution has been suspended, the JavaScript engine isnāt using any CPU resources for these particular functions and is free to execute other tasks in the meantime.
Your code gets the benefit of following a more elegant syntax of code for getting the result from a promise thanĀ promise.then()
, and you (as a programmer) are left with a syntax that is easier and cleaner to read and write.
How to Use Async/Await in JavaScript
There are multiple great resources on how you can use async/await in your javaScript code/projects.
Here is one by Catalinās Tech that I believe captures the essence perfectly.
Both resources have been linked at the end of the story under suggested resources.
And since Iām a huge fan of fireship tutorials in their ability to explain complex topics in a beginner-friendly manner, here is a short video from Jeff Delaney (12 mins).
In contrast, other async/await videos you will find on YouTube are anywhere between 40 mins and 1.5 hours long.
Why do I include fireship tutorial videos over other examples? Just look at the comments:
Should You Choose Async/Await Over Promises?
It is a highly personal choice, and there are people on both sides of the aisle here.
There are those who swear by the amazing value async/await adds to your code over using plain old Promises. And then there are those who think async/await was spawned in the fires of hell.
Here is one such point many developers make against using async/await:
Are they right or wrong? You will have to make that call on a case-to-case basis, depending on what you are doing with your code.
But, speaking from personal experience, I am extremely indifferent to whether I am using async/await
or Promise.then()
. I have used both in my codes, and I find them both to be quite easy to use.
But, are there scenarios where I prefer async/await? Yes. When I end up tumbling down a rabbit hole of thens and callbacks. I find async/await
to be a more cleaner solution in such cases.
When you come back to your code a few weeks later, or when you have another developer working on the same codebase, cleaner, neater and shorter code does have its advantage.
The bottomline? If clean and crisp code matters to you, do choose async/await
. Your code looks different, cleaner, neater, and shorter. And the error handling simplicity improves.
But, if you were thinking of choosing async/await
over Promise.then()
because of some major performance enhancements, you would find yourself disappointed.
Async/Await essentially does the same thing that your Promise.then()
code blocks do.
So, yes, your code is going to look different, but the logic flow and capabilities will stay the same.
Alright then. Now that we have briefly touched upon all there is to know about async/await, letās get back to the matter at hand, and try fixing the problem we ran into with our async/await
usage in forEach()
earlier.
How to fix async/await usage with forEach() in JavaScript?
Alright.
The promo-code sending loop we saw earlier. Letās fix that now.
#1. The forā¦of solution
const subscribedUsers = await this.fetchYesterdaysSubscriptions();
for (const user of subscribedUsers) {
await sendPromoCode(user);
}
await sendEmailToAdmin("All new subscribers have been sent the promo code");
This loop will wait for one promo-code email to be sent before moving on with the next iteration.
PS: The awesome guys at Airbnb released a JavaScript Style guide. It follows and recommends what they call āa mostly reasonable approach to JavaScriptā. The style guide is available at github as well. It is extremely popular, and is followed by thousands of JavaScript developers worldwide. If you werenāt aware of it, I would highly recommend taking a look at it. Anyway, the reason I bring up the Airbnb style guide is for the simple fact that it recommends not using
forā¦of
for web apps since it requires a large polyfill. If you prefer, you can use anArray.reduce()
as well to achieve the same effect.
#2. The Array.reduce() solution
May the gods at Airbnb be happy with this solution!
const subscribedUsers = await this.fetchYesterdaysSubscriptions();
await subscribedUsers.reduce(async (referencePoint, user) => {
// Check for execution status of previous iteration, i.e. wait for it to get finished
await referencePoint;
// Process the current iteration
await sendPromoCode(user);
}, Promise.resolve());
await sendEmailToAdmin("All new subscribers have been sent the promo code");
We are used to using a second argument as an output from the previous code execution ā a summary of how things went, if you may. But, in this particular case, things are a bit different.
We are using the argument ā referencePoint ā just as a means to pass the promise from the previous iterationās callback to the next iteration.
This helps our code to wait for the previous iteration to finish before it starts processing the next one.
Iām not a huge fan of this approach.
While there is nothing wrong with it, and it behaves almost exactly the same as theĀ for...of
Ā example we saw in solution #1, I find this solution to be:
-
tedious
-
hard to read
-
slightly confusing ā especially for a beginner
But, this is the approach recommended by the Airbnb JavaScript style guide. Probably because it will reduce your browser bundle size since it doesnāt use iterations the way forā¦of
does.
Some browsers do require a polyfill when your code has iterators, and polyfill does indeed pack a punch. Having said that, if I had to choose between bundle size and my convenience, Hello convenience! š
The traditionalĀ for
Ā approach, as well as theĀ Array.reduce()
Ā approach we just looked at, processes your code in series. It sends one promo mail, waits for that promise to get resolved, then moves on to the next user.
But in a lot of cases (for example, this one itself), the order in which the loop is iterated through isnāt important.
You just want each user to receive a promo code independently.
So, instead of a series-processing, it is quicker to cycle through all new subscribers in parallel. Which brings us to our next solution in the list.
#3. The Array.map() solution
const subscribedUsers = await this.fetchYesterdaysSubscriptions();
await Promise.all(
subscribedUsers.map(async (user) => {
await sendPromoCode(user);
})
);
await sendEmailToAdmin("All new subscribers have been sent the promo code");
This solution is elegant since it starts the process of sending the promo code to all users at once, and at the same time. But, it will wait for every single one of them to finish executing before it will move on to send the email to admin.
And now, let us finally move on to the show stopper for the night.
Using async/await with forEach()
#4. async/await, with forEach()ā¦done the right way
Spoiler alert: It is horrible! Just use #1 or #3. I am not a huge fan of #2 (sorry Airbnb masters), and this one isā¦justā¦theā¦worst!
const subscribedUsers = await this.fetchYesterdaysSubscriptions();
async function customForEachFunction(arrayInput, callback) {
arrayInput.forEach(async (item) => {
await callback(item, arrayInput);
});
}
const forEachSolution = async () => {
await customForEachFunction(subscribedUsers, async (user) => {
await sendPromoCode(user);
});
await sendEmailToAdmin("All new subscribers have been sent the promo code");
};
forEachSolution();
Thatās disgusting!
It may work, but that doesnāt make it any less disgusting.
Conclusion
Donāt try to make something do what it isnāt supposed to do.
forEach() was never designed to work for asynchronous code. So if you try to make it do that, you will need to do some extra legwork just to make it happen.
And what is the point of doing all that when you have better, simpler, easier means of achieving the same output.
Over to you now.
-
Have you run into an issue with async/await because you did not realise you were using it with a forEach()?
-
What did you break during that time, as you tried to figure things out?
What more would you have wanted me to cover here? How do you go about using async/await in an iterable scenario? Let me know in the comments!