Por que seu async/await silencia no forEach? Entendendo de primeira vista
\n\nVocê já escreveu algo como este código, esperando que ele executasse operações assíncronas em ordem?
\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('Pronto!');\n\nE recebeu "Pronto!" antes dos dados carregarem? Familiar? Você não está sozinho. Esta é uma das perguntas mais populares no Stack Overflow com milhares de visualizações. O problema é que forEach é um método síncrono. Ele apenas executa os callbacks, mas não espera que eles terminem. Suas funções async iniciam, mas o forEach já segue em frente, como um velocista que não se importa com a chegada dos outros.
Neste artigo, não vamos apenas mostrar como corrigir isso. Vamos analisar por que isso acontece e dar a você três soluções funcionais com exemplos. Você aprenderá quando usar Promise.all e quando usar o bom e velho for...of. Vamos lá!
Anatomia do problema: forEach e promessas
\n\nVamos dar uma olhada nos bastidores. O método forEach em JavaScript é estruturado assim:
Array.prototype.forEach = function(callback) {\n for (let i = 0; i < this.length; i++) {\n callback(this[i], i, this);\n }\n};\n\nViu? Ele apenas chama o callback para cada elemento, mas não retorna nada. Mesmo que o callback retorne uma Promise, o forEach a ignora. Ele não espera, não coleta resultados — apenas executa e esquece.
Quando você escreve async (user) => { ... }, sua função retorna uma Promise. Mas o forEach não sabe o que fazer com ela. No final, todas as requisições são iniciadas em paralelo (se for fetch), mas você não pode esperar que terminem. O código após o forEach é executado instantaneamente.
Exemplo real: como isso parece na prática
\n\nImagine que você está processando um array de IDs de produtos e para cada um precisa obter o preço do banco de dados:
\n\nconst productIds = [101, 102, 103];\n\nasync function getPrice(id) {\n // Simulação de requisição ao BD\n return new Promise(resolve => {\n setTimeout(() => {\n resolve(`Preço para ${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('Todos os preços carregados!'); // Será executado imediatamente, não após 3 segundos\n\nResultado no console:
\nTodos os preços carregados!\nPreço para 101: $42\nPreço para 102: $78\nPreço para 103: $15\n\nConcordo, não é o que esperávamos. A boa notícia: existem soluções simples e elegantes.
\n\nSolução nº1: for...of — execução sequencial
\n\nA maneira mais simples de fazer o async/await funcionar em um loop é usar for...of. Ao contrário do forEach, o for...of funciona com async/await nativamente, porque você controla o fluxo de execução dentro do bloco.
const productIds = [101, 102, 103];\n\nasync function getPrice(id) {\n return new Promise(resolve => {\n setTimeout(() => {\n resolve(`Preço para ${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('Todos os preços carregados!');\n}\n\nloadAllPrices();\n\nAgora o resultado será:
\nPreço para 101: $42\n(após 1 segundo)\nPreço para 102: $78\n(após 1 segundo)\nPreço para 103: $15\n(após 1 segundo)\nTodos os preços carregados!\n\nVantagens: simplicidade, legibilidade, ordem previsível.
\nDesvantagens: lento — cada requisição espera a conclusão da anterior.