Async/await y Array.map: cuando los mejores amigos se convierten en enemigos
\n\n¿Alguna vez ha escrito algo como const results = await array.map(async (item) => {...}) y obtuvo un array de Promise en lugar de los datos esperados? Si es así, no está solo. Esta pregunta en Stack Overflow ha acumulado decenas de miles de visitas, y la cantidad de desarrolladores que han caído en este error se cuenta por millones.
En este artículo analizaremos los 5 errores más peligrosos al usar async/await con Array.map en TypeScript. Y, lo que es más importante, le mostraré cómo evitarlos. Abróchese el cinturón: será doloroso, pero útil.
Error n.º 1: Olvidar Promise.all (el más común)
\n\nEl error más común es pensar que await dentro de map esperará mágicamente a todas las promesas. Spoiler: no, no lo hará.
El problema
\n// ❌ Así no funciona\nconst users = [1, 2, 3, 4, 5];\nconst userData = await users.map(async (id) => {\n const response = await fetch(`/api/users/${id}`);\n return response.json();\n});\n// userData es Promise[], ¡no UserData[]!\nconsole.log(userData); // [Promise, Promise, Promise, Promise, Promise]\n\n¿Por qué ocurre esto?
\nArray.map es síncrono. No espera a que los callbacks asíncronos terminen. Cada llamada a la función async devuelve una Promise, y map simplemente las agrupa en un array. await antes de map solo espera a que termine el propio map, pero no a sus callbacks.
Solución
\n// ✅ Correcto: envolvemos en Promise.all\nconst users = [1, 2, 3, 4, 5];\nconst userData = await Promise.all(\n users.map(async (id) => {\n const response = await fetch(`/api/users/${id}`);\n return response.json();\n })\n);\n// userData es UserData[]! 🎉\nconsole.log(userData);\n\nError n.º 2: Todas las promesas se ejecutan en paralelo (cuando se necesita secuencial)
\n\nPromise.all ejecuta todas las promesas en paralelo. Esto es genial para el rendimiento, pero a veces necesitamos ejecutar las solicitudes estrictamente una tras otra. Por ejemplo, al trabajar con una API que limita la cantidad de solicitudes, o cuando cada solicitud siguiente depende de la anterior.
El problema
\n// ❌ Todas las solicitudes se envían al mismo tiempo\nconst ids = [1, 2, 3, 4, 5];\nconst results = await Promise.all(\n ids.map(async (id) => {\n // 5 solicitudes a la API simultáneamente: se puede obtener un rate limit\n return await api.processItem(id);\n })\n);\n\nSolución: ejecución secuencial
\n// ✅ Usamos for...of con await\nconst ids = [1, 2, 3, 4, 5];\nconst results: ItemData[] = [];\n\nfor (const id of ids) {\n const result = await api.processItem(id);\n results.push(result);\n}\n// results es un array con datos procesados en orden\n\nUna alternativa es Array.reduce con un acumulador de promesa:
// ✅ También funciona, pero es menos legible\nconst ids = [1, 2, 3, 4, 5];\nconst results = await ids.reduce(async (accPromise, id) => {\n const acc = await accPromise;\n const result = await api.processItem(id);\n return [...acc, result];\n}, Promise.resolve([] as ItemData[]));\n\nError n.º 3: Tipado incorrecto en TypeScript
\n\nTypeScript puede no entender de inmediato lo que está haciendo con un map asíncrono. Especialmente si usa Promise.all sin especificar explícitamente el tipo.
El problema
\n// ❌ TypeScript se queja o deduce un tipo incorrecto\nconst ids = [1, 2, 3];\nconst results = await Promise.all(\n ids.map(async (id) => {\n const data = await fetchData(id);\n return data.name; // TypeScript podría pensar que es Promise<string>\n })\n);\n// results tiene tipo unknown o Promise<string>[]\n\nSolución: anotación de tipo explícita
\n// ✅ Especificamos explícitamente el tipo de resultado