forEach no espera nada
Un callback async dentro de forEach no espera nada. Tu bucle termina antes de que el trabajo termine — y como el código se ve correcto, este es uno de los bugs asíncronos más comunes que un asistente de IA escribe felizmente por ti.
Por qué forEach no puede esperar
MDN lo dice sin rodeos: "forEach() expects a synchronous function — it does not wait for promises." Una función async siempre devuelve una promesa, pero forEach descarta ese valor de retorno. Llama a tu callback una vez por elemento, uno tras otro, y sigue adelante. El resultado:
const ratings = [5, 4, 5];
let sum = 0;
ratings.forEach(async (rating) => {
sum = await sum + rating;
});
console.log(sum); // 0 — not 14
Cada callback se inicia; ninguno termina antes de que corra el console.log. Los fallos peores tienen la misma forma: escrituras a la base de datos que llegan después de enviar la respuesta, archivos cerrados mientras todavía se escriben, un try/catch alrededor del bucle que no atrapa nada porque los rechazos escapan como unhandled promise rejections.
Elige tu intención: dos patrones que sí esperan
La solución no es un wrapper ingenioso sobre forEach — es declarar qué comportamiento quieres.
Secuencial — importan el orden y la contrapresión. Usa for...of con await adentro. Cada iteración termina antes de que empiece la siguiente, los errores aparecen en el await, y nunca disparas 10,000 solicitudes a la vez:
for (const user of users) {
await sendEmail(user); // one at a time, in order
}
Concurrente — el orden no importa. Usa Promise.all sobre un map. Todo el trabajo arranca de inmediato, y la función que lo rodea se pausa hasta que todas las promesas se resuelven:
const results = await Promise.all(users.map((u) => fetchProfile(u.id)));
Promise.all es fail-fast: se rechaza en cuanto cualquiera de las promesas de entrada se rechaza, con ese primer error. Cuando necesitas todos los resultados aunque algunos fallen, usa Promise.allSettled, que siempre espera y devuelve {status, value|reason} por elemento.
Reglas prácticas
| Situación | Usa | Por qué |
|---|---|---|
| Importa el orden, o cada paso depende del anterior | for...of + await | Estrictamente secuencial, manejo natural de errores |
| Trabajo independiente, lo quieres rápido, un fallo debe abortar | await Promise.all(items.map(fn)) | Concurrente, fail-fast |
| Trabajo independiente, necesitas cada resultado aunque haya fallos | await Promise.allSettled(items.map(fn)) | Nunca corta antes de tiempo |
| Efectos secundarios síncronos sobre un valor síncrono | forEach (callback no async) | Bien — no hay nada que esperar |
Cualquier callback async | nunca forEach | La promesa se descarta |
Notas para usuarios avanzados
- Concurrencia acotada.
Promise.allsobre 5,000 elementos lanza 5,000 solicitudes a la vez. Divide el arreglo en bloques y aplicaPromise.alla cada bloque dentro de unfor...of, o usa una pequeña utilidad de pool — obtienes concurrencia y contrapresión. mapsinawaites el mismo bug.items.map(async ...)sinawait Promise.all(...)produce un arreglo de promesas pendientes que nunca miras. Elmapno es la solución; elawait Promise.allsí.for await...ofes para iterables asíncronos (streams, APIs paginadas) — fuentes que producen valores de forma asíncrona. Para un arreglo normal que procesas asincrónicamente,for...ofconawaitadentro es la herramienta correcta.- Ponle lint. Al revisar código generado por IA, busca
forEach(async— casi siempre está mal. Reglas comono-misused-promises(typescript-eslint) marcan promesas pasadas donde se esperavoid.
Recursos
- Array.prototype.forEach() — MDN — la advertencia "expects a synchronous function" y el ejemplo de
sum === 0 - Promise.all() — MDN — semántica de concurrencia y rechazo fail-fast
- Promise.allSettled() — MDN — espera todos los resultados
- for...of — MDN — el patrón secuencial
- for await...of — MDN — iterar iterables asíncronos
¿Construyendo una funcionalidad con IA? Yeda AI diseña, audita y entrega sistemas LLM de producción.