¿Por qué tu async/await se calla en forEach? Analicémoslo de un vistazo
\n\n¿Alguna vez has escrito algo como este código, esperando que ejecutara operaciones asíncronas en orden?
\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('¡Listo!');\n\n¿Y obtenías "¡Listo!" antes de que se cargaran los datos? ¿Te suena? No estás solo. Esta es una de las preguntas más populares en Stack Overflow con miles de visitas. El problema es que forEach es un método síncrono. Simplemente ejecuta los callbacks, pero no espera a que terminen. Tus funciones async se inician, pero forEach ya sigue adelante, como un velocista al que no le importa la meta de los demás.
En este artículo no solo mostraremos cómo solucionarlo. Analizaremos por qué sucede y te daremos tres soluciones funcionales con ejemplos. Aprenderás cuándo usar Promise.all y cuándo usar el clásico for...of. ¡Allá vamos!
Anatomía del problema: forEach y las promesas
\n\nMiremos bajo el capó. El método forEach en JavaScript está diseñado así:
Array.prototype.forEach = function(callback) {\n for (let i = 0; i < this.length; i++) {\n callback(this[i], i, this);\n }\n};\n\n¿Ves? Simplemente llama a callback para cada elemento, pero no devuelve nada. Incluso si el callback devuelve una Promise, forEach la ignora. No espera, no recoge resultados — solo la ejecuta y la olvida.
Cuando escribes async (user) => { ... }, tu función devuelve una Promise. Pero forEach no sabe qué hacer con ella. Como resultado, todas las peticiones se inician en paralelo (si es fetch), pero no puedes esperar a que terminen. El código después de forEach se ejecuta instantáneamente.
Ejemplo real: cómo se ve en la práctica
\n\nImagina que procesas un array de IDs de productos y para cada uno necesitas obtener el precio de la base de datos:
\n\nconst productIds = [101, 102, 103];\n\nasync function getPrice(id) {\n // Simulación de una consulta a la BD\n return new Promise(resolve => {\n setTimeout(() => {\n resolve(`Precio 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 los precios cargados!'); // Se ejecuta inmediatamente, no después de 3 segundos\n\nResultado en la consola:
\n¡Todos los precios cargados!\nPrecio para 101: $42\nPrecio para 102: $78\nPrecio para 103: $15\n\n¿Estás de acuerdo? No es lo que esperábamos. La buena noticia: hay soluciones simples y elegantes.
\n\nSolución n.º 1: for...of — ejecución secuencial
\n\nLa forma más sencilla de hacer que async/await funcione en un bucle es usar for...of. A diferencia de forEach, for...of funciona con async/await de forma nativa, porque tú controlas el flujo de ejecución dentro del bloque.
const productIds = [101, 102, 103];\n\nasync function getPrice(id) {\n return new Promise(resolve => {\n setTimeout(() => {\n resolve(`Precio 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 los precios cargados!');\n}\n\nloadAllPrices();\n\nAhora el resultado será:
\nPrecio para 101: $42\n(después de 1 segundo)\nPrecio para 102: $78\n(después de 1 segundo)\nPrecio para 103: $15\n(después de 1 segundo)\n¡Todos los precios cargados!\n\nVentajas: simplicidad, legibilidad, orden predecible.
\nDesventajas: lento — cada petición espera a que termine la anterior.