Why and How to Avoid Await in a For-Loop

Chris Fields II
5 min readOct 19, 2019

--

Stop for a minute and make sure your scripts are running AFAP (As Fast As Possible)

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.

A code snippet demonstrating a bad example usage of await within a for-loop
An example of bad usage of await within an for-loop.

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!

An example of good usage of avoiding await in an for-loop for max efficiency and using Promise.all()

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!

An example of good usage of Array.forEach() to start async functions if you don’t care about accumulating the responses

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.

An image of purposefully using await within an for-loop to pause execution

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

--

--

Chris Fields II

I am a fullstack developer currently building the new GameStop mobile app & writing my experiences