Are there any issues with using `async`/`await` with `forEach()` loops in JavaScript?

David Y.

The Problem

Consider the following JavaScript code:

const requests = await getRequests(); await requests.forEach(async (request) => { await process(request); }); await notifyCompletion("All requests processed.");

At first glance, this code appears straightforward. We retrieve a set of request objects, use a forEach() loop to process them, and call notifyCompletion() once our processing is done. All of our custom functions are async, so we correctly await their results.

But if we run this code, we may start to encounter unexpected behavior. What’s wrong with it?

The main problem is that Array.prototype.forEach() predates the implementation of the async/await pattern, which relies on Promises. forEach() does not return a Promise, so the await in front of requests.forEach() will do nothing.

A second problem is that await in await process(request) is inside a callback function, and thus will not halt execution of the outer function where forEach() resides. forEach() will invoke its callback function for each request in requests as quickly as possible, without waiting for any of the previous process() calls to return. This means our requests will be processed in parallel rather than in sequence, which may or may not be what we want.

An additional, more serious consequence of the forEach() loop not pausing execution to wait for the results of process(), is that our code may execute notifyCompletion() before all requests have been fully processed. In the best case scenario, our code will send a completion notification slightly before all requests have been handled; in the worst, our code will send a completion notification slightly before crashing from an uncaught error in one of the process() calls.

The Solution

How can we rewrite this code to do what it appears to be doing? There are a couple of different approaches we can take, depending on whether we would like requests to be processed in sequence or in parallel.

To process requests in sequence, we can replace forEach() with JavaScript’s for...of statement (introduced in 2015):

const requests = await getRequests(); for (const requests of requests) { await process(request); } await notifyCompletion("All requests processed.");

Individual iterations of this loop are part of the same function as the surrounding code, and so the await inside the loop will cause execution to pause on every iteration of the loop. notifyCompletion() will only execute once all iterations of the loop have completed.

To process requests in parallel, we can use Promise.all() and Array.prototype.map():

const requests = await getRequests(); await Promise.all(requests.map(async (request) => { await process(request); })); await notifyCompletion("All requests processed.");

Promise.all(), unlike forEach(), can be awaited, and so we can be assured that all of our requests will be processed before notifyCompletion() is called.

Get Started With Sentry

Get actionable, code-level insights to resolve JavaScript performance bottlenecks and errors.

  1. Create a free Sentry account

  2. Create a JavaScript project and note your DSN

  3. Grab the Sentry JavaScript SDK

<script src="https://browser.sentry-cdn.com/7.110.1/bundle.min.js"></script>
  1. Configure your DSN
Sentry.init({ dsn: 'https://<key>@sentry.io/<project>' });

Loved by over 4 million developers and more than 90,000 organizations worldwide, Sentry provides code-level observability to many of the world’s best-known companies like Disney, Peloton, Cloudflare, Eventbrite, Slack, Supercell, and Rockstar Games. Each month we process billions of exceptions from the most popular products on the internet.

Share on Twitter
Bookmark this page
Ask a questionJoin the discussion

Related Answers

A better experience for your users. An easier life for your developers.

    TwitterGitHubDribbbleLinkedinDiscord
© 2024 • Sentry is a registered Trademark
of Functional Software, Inc.