Async/await in forEach: How to Make Asynchronous Loops Work Together

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Why Is Your async/await Silent in forEach? Let's Figure It Out at First Glance

\n\n

Have you ever written something like this code, hoping it would execute asynchronous operations in order?

\n\n
const users = ['alice', 'bob', 'charlie'];\n\nusers.forEach(async (user) => {\n  const data = await fetch(`https://api.example.com/users/${user}`);\n  console.log(await data.json());\n});\n\nconsole.log('Done!');
\n\n

And got "Done!" before the data loaded? Sound familiar? You are not alone. This is one of the most popular questions on Stack Overflow with thousands of views. The problem is that forEach is a synchronous method. It simply runs callbacks but does not wait for them to complete. Your async functions start, but forEach has already moved on, like a sprinter who doesn't care about the finish line of others.

\n\n

In this article, we won't just show you how to fix it. We'll break down why this happens and give you three working solutions with examples. You'll learn when to use Promise.all and when to use good old for...of. Let's go!

\n\n

Anatomy of the Problem: forEach and Promises

\n\n

Let's look under the hood. The forEach method in JavaScript works like this:

\n\n
Array.prototype.forEach = function(callback) {\n  for (let i = 0; i < this.length; i++) {\n    callback(this[i], i, this);\n  }\n};
\n\n

See? It simply calls callback for each element, but returns nothing. Even if the callback returns a Promise, forEach ignores it. It doesn't wait, doesn't collect results — it just fires and forgets.

\n\n

When you write async (user) => { ... }, your function returns a Promise. But forEach doesn't know what to do with it. As a result, all requests are launched in parallel (if it's fetch), but you cannot wait for them to complete. The code after forEach executes instantly.

\n\n

Real Example: What It Looks Like in Practice

\n\n

Imagine you are processing an array of product IDs and need to get the price from the database for each one:

\n\n
const productIds = [101, 102, 103];\n\nasync function getPrice(id) {\n  // Simulating a database request\n  return new Promise(resolve => {\n    setTimeout(() => {\n      resolve(`Price for ${id}: $${Math.floor(Math.random() * 100)}`);\n    }, 1000);\n  });\n}\n\nproductIds.forEach(async (id) => {\n  const price = await getPrice(id);\n  console.log(price);\n});\n\nconsole.log('All prices loaded!'); // Executes immediately, not after 3 seconds
\n\n

Result in the console:

\n
All prices loaded!\nPrice for 101: $42\nPrice for 102: $78\nPrice for 103: $15
\n\n

Admit it, this is not what we expected. The good news: there are simple and elegant solutions.

\n\n

Solution #1: for...of — Sequential Execution

\n\n

The simplest way to make async/await work in a loop is to use for...of. Unlike forEach, for...of works with async/await natively because you control the execution flow inside the block.

\n\n
const productIds = [101, 102, 103];\n\nasync function getPrice(id) {\n  return new Promise(resolve => {\n    setTimeout(() => {\n      resolve(`Price for ${id}: $${Math.floor(Math.random() * 100)}`);\n    }, 1000);\n  });\n}\n\nasync function loadAllPrices() {\n  for (const id of productIds) {\n    const price = await getPrice(id);\n    console.log(price);\n  }\n  console.log('All prices loaded!');\n}\n\nloadAllPrices();
\n\n

Now the result will be:

\n
Price for 101: $42\n(after 1 second)\nPrice for 102: $78\n(after 1 second)\nPrice for 103: $15\n(after 1 second)\nAll prices loaded!
\n\n

Pros: simplicity, readability, predictable order.

\n

Cons: slow — each request waits for the previous one to complete.

Blogs

Book Recommendations