Why and How to Avoid Await in a For-Loop
Ever come across a moment where you have an array of items and you need to make an asynchronous API call for each item? Me too! Several times. There are many ways to accomplish this task but some are better than others. This article will cover examples of The Bad, The Good, and The Exceptions.
Hint: If you are reading this from a browser, there will be small snippets of code you can copy/paste into the Console (F12) to see the results yourself.
A big influence for me writing this article is remembering reading an article (on Medium of course) when async await
first came into the scene and the writer mentioned that using await
within an for-loop
actually made the code synchronous. But wait😲, its an asynchronous function! What do you mean it makes it synchronous?! I learned a lot in that article, but I was still left with that question. I’m hoping to answer it if you are still wondering the same. Let’s delve into it with code!
You can view the code discussed here. The code imitates fetching multiple Users from a database by a User ID. Go ahead and copy/paste the whole script into your Console (F12) if you are on a browser and test the different functions.
The Bad
The bad way of using await
within an for-loop
is how I always handled this task until I worked on a project with the eslint rule no-await-in-loop
. Duhhh! A for-loop
is a synchronous structure. That defeats the whole purpose of our multiple async
functions and taking full advantage of concurrency, which is when tasks start, run, and complete in overlapping time periods, in no specific order.
We don’t want to run the async
functions one after another, we want them to all be running at the same time and resolve (or error) at about the same time.
In the following code snippet, although we accomplish our task of getting users into an array for “further processing” aka console.log
, it actually takes way longer than one would hope for. The console.timeEnd(‘badForLoop’)
printed 15016.52490234375ms (15 seconds) when getting 5 Users.
This is because we synchronously call an asynchronous function that takes <X> amount of seconds to return/error before the next line of code can be executed. In this case getUser
is as asynchronous function that is hardcoded to return a user in 3 seconds. With some simple math that means this badForLoop
will take at a minimum numIds * 3
seconds to fill the users
array. This is the Why, efficiency.
The Good
There are a few good ways to accomplish this task depending on your use case.
The overall goal is to simply call all of our async
functions at about the same time so they can run concurrently for max efficiency.
CASE 1: If you care about the responses from the async
functions then this is the case for you!
Case 1 is How I now handle this task 99% of the time. If you are able to run the functions from the repo within your favorite JS environment/Console (F12) then you will see the difference between the badForLoop
example getting 5 Users (15016.52490234375ms) and the goodForLoop
example getting 5 Users (3005.02880859375ms)! Both ultimately get the job done which is an array of Users for further processing but one is a lot more efficient than the other in doing so.
The key here is using the for-loop
to start the async
functions and pushing the pending promises into an array so they can work concurrently. We’ll await
that array with Promise.all(promises)
to get all of the promises responses where we could do further processing.
CASE 2: If you don’t care about accumulating the responses from the async
functions but just want to efficiently start some async
functions then this is the case for you!
It takes 0.44287109375ms for the console.timeEnd('goodForEachLoop')
to log when getting 5 Users. This is due to tagging the function passed into the forEach()
as async
which immediately returns a Promise
and proceeds to the next iteration. The downfall of this case though is it’s not so straight forward to accumulate the Users into the array for processing after the forEach()
without resorting to some trickery.
The Exceptions
Ok so sometimes you actually might not want ALL of your async functions to start at the same time due to various reasons including: API Limits (we may have to throttle time between requests), Device Resources (we may have to throttle requests to write files onto device storage especially for mobile devices), etc. In these cases using await
within an for-loop
may actually be advantageous!
In the few times I’ve found myself wanting to pause an for-loop
I use the following code snippet to do so.
The End
Hopefully I’ve helped explain some of the different use cases of await
with an for-loop.
If it is still somewhat confusing, I highly suggest playing around with the test functions in this repo.
Resources
Promise
: native Promise object- Bluebird.js : A Promise library with more exotic functions than the native
Promise
object. (I rarely use this as the nativePromise
object is usually enough but figured to link it just in case) - Repo with code examples: https://github.com/ChrisLFieldsII/medium-await-for-loop
- ESLint
no-await-for-loop
rule: https://eslint.org/docs/rules/no-await-in-loop