Why Is Your async/await Silent in forEach? Let's Figure It Out at First Glance
\n\nHave you ever written something like this code, hoping it would execute asynchronous operations in order?
\n\nconst 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\nAnd 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.
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!
Anatomy of the Problem: forEach and Promises
\n\nLet's look under the hood. The forEach method in JavaScript works like this:
Array.prototype.forEach = function(callback) {\n for (let i = 0; i < this.length; i++) {\n callback(this[i], i, this);\n }\n};\n\nSee? 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.
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.
Real Example: What It Looks Like in Practice
\n\nImagine you are processing an array of product IDs and need to get the price from the database for each one:
\n\nconst 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\nResult in the console:
\nAll prices loaded!\nPrice for 101: $42\nPrice for 102: $78\nPrice for 103: $15\n\nAdmit it, this is not what we expected. The good news: there are simple and elegant solutions.
\n\nSolution #1: for...of — Sequential Execution
\n\nThe 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.
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\nNow the result will be:
\nPrice 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\nPros: simplicity, readability, predictable order.
\nCons: slow — each request waits for the previous one to complete.