Original article: All you need to know about Promise.all

Las promesas en JavaScript son una de las APIs poderosas que nos ayudan hacer operaciones asíncronas.

Promise.all toma operaciones asíncronas al siguiente nuevo nivel, ya que te ayuda agregar un grupo de promesas.

En otras palabras, puedo decir que te ayuda en hacer operaciones concurrentes (a veces gratis).

Prerequisitos:

Tienes que saber qué es una Promesa en JavaScript.

¿Qué es Promise.all?

Promise.all es en realidad una promesa que toma un arreglo de promesas como una entrada (un iterable). Luego se resuelve cuando todas las promesas se resuelven o si cualquiera de ellos es rechazado.

Por ejemplo, digamos que tienes diez promesas (una operación asíncrona para ejecutar una llamada de red o una conexión de base de datos). Tienes que saber cuando todas las promesas se resuelven o si tienes que esperar hasta que todas las promesas se resuelvan. Así que estás pasando todas las diez promesas a Promise.all. Después, el mismo Promise.all como una promesa se resolverá una vez que todas las diez promesas se resuelvan o si alguna de las diez promesas son rechazadas con un error.

Veámoslo en código:

Promise.all([Promesa1, Promesa2, Promesa3])
 .then(resultado) => {
   console.log(resultado)
 })
 .catch(error => console.log(`Error en las promesas ${error}`))

Como puedes ver, estamos pasando un arreglo a Promise.all. Y cuando todas las tres promesas se resuelvan, Promise.all se resuelve y la salida es impresa.

Veamos un ejemplo:

// Una simple promesa que resuelve después de un tiempo dado
const timeOut = (t) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(`Completado en ${t}`)
    }, t)
  })
}

// Resolviendo una promesa normal.
timeOut(1000)
 .then(result => console.log(result)) // Completado en 1000

// Promise.all
Promise.all([timeOut(1000), timeOut(2000)])
 .then(result => console.log(result)) // ["Completado en 1000", "Completado en 2000"]

En el ejemplo de arriba, Promise.all es resuelto después de 2000ms y la salida es impresa como un arreglo.

Una cosa interesante sobre Promise.all es que el orden de las promesas se mantiene. La primera promesa en el arreglo se resolverá como primer elemento del arreglo de salida, la segunda promesa será el segundo elemento en el arreglo de salida y así sucesivamente.

Veamos otro ejemplo:

// Una simple promesa que se resuelve después de un tiempo dado
const timeOut = (t) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(`Completado en ${t}`)
    }, t)
  })
}

const duraciones = [1000, 2000, 3000]

const promesas = []

duraciones.map((duracion) => {
  // En la linea de abajo, dos cosas suceden.
  // 1. Estamos llamando la función asíncrona (timeout()). Así que en este punto la función asíncrona ha comenzado e ingresa el estado 'pendiente'.
  // 2. Estamos empujando la promesa pendiente a un arreglo.
  promesas.push(timeOut(duracion)) 
})

console.log(promesas) // [ Promise { "pending" }, Promise { "pending" }, Promise { "pending" } ]

// Estamos pasando un arreglo de promesas pendientes a Promise.all
// Promise.all esperará hasta que todas las promesas se resuelvan y luego él mismo se resuelve.
Promise.all(promesas)
.then(response => console.log(response)) // ["Completado en 1000", "Completado en 2000", "Completado en 3000"]

Del ejemplo de arriba, es claro que Promise.all espera hasta que todas las promesas se resuelvan.

Veamos qué sucede si algunas de las promesas se rechazan.

// Una simple promesa que se resuelve después de un tiempo dado
const timeOut = (t) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (t === 2000) {
        reject(`Rechazado en ${t}`)
      } else {
        resolve(`Completado en ${t}`)
      }
    }, t)
  })
}

const duraciones = [1000, 2000, 3000]

const promesas = []

duraciones.map((duracion) => {
  promesas.push(timeOut(duracion)) 
})

// Estamos pasando un arreglo de promesas pendientes a Promise.all
Promise.all(promesas)
.then(response => console.log(response)) // Promise.all no puede ser resuelto, ya que una de las promesas pasada se rechazó.
.catch(error => console.log(`Error al ejecutar ${error}`)) // Promise.all tira un error.

Como puedes ver, si una de las promesas falla, luego todo el resto de las promesas fallan. Entonces Promise.all es rechazado.

Para algunos casos de uso, no necesitas eso. Necesitas ejecutar todas las promesas, inclusive si algunas han fallado, o tal vez puedes manejar las promesas fallidas luego.

Veamos cómo manejar eso.

const duraciones = [1000, 2000, 3000]

promesas = duraciones.map((duracion) => {
  return timeOut(duracion).catch(e => e) // Manejar el error para cada promesa.
})

Promise.all(promesas)
  .then(response => console.log(response)) // ["Completado en 1000", "Completado en 2000", "Completado en 3000"]
  .catch(error => console.log(`Error al ejecutar ${error}`))

Casos de uso de Promise.all

Digamos que tienes que realizar un número grande de operaciones asíncronas como enviar un bulto de mails de marketing a miles de usuarios.

Un simple pseudo-código sería:

for (let i=0;i<50000; i += 1) {
 enviarMailParaUsuario(usuario[i]) // operación asíncrona para enviar un mail
}

El ejemplo de arriba es directo. Pero no rinde muy bien. El stack se pondrá muy cargado en algún punto del tiempo, JavaScript tendrá un gran número de conexiones HTTP abiertas, lo cual mataría al servidor.

Un enfoque con más rendimiento sería hacerlo en lotes. Toma primero 500 usuarios, dispara el mail y espera hasta que todas las conexiones HTTP estén cerradas. Y luego toma el siguiente lote para procesarlo y así sucesivamente.

Veamos un ejemplo:

// Función asíncrona para enviar el mail a una lista de usuarios.
const enviarMailParaUsuarios = async (usuarios) => {
  const longitudUsuarios = usuarios.length
  
  for (let i = 0; i < longitudUsuarios; i += 100) { 
    const peticiones = usuarios.slice(i, i + 100).map((usuario) => { // El tamaño del lote es 100. Estamos procesando un conjunto de 100 usuarios.
      return dispararMailParaUsuario(usuario) // Función asíncrona para enviar el mail.
       .catch(e => console.log(`Error al enviar el email para el ${usuario} - ${e}`)) // Capta el error si algo va mal. Así no bloqueará el bucle.
    })
    
    // las peticiones tendrán 100 o menos promesas pendientes. 
    // Promise.all esperará hasta que todas las promesas se resuelvan y después toma el siguiente 100.
    await Promise.all(peticiones)
     .catch(e => console.log(`Error al enviar el mail para el lote ${i} - ${e}`)) // Capta el error.
  }
}


enviarMailParaUsuarios(listasUsuarios)

Consideremos otro escenario. Tienes que construir una API que obtiene información de múltiples APIs de terceros y agrega todas las respuestas de las APIs.

Promise.all es la forma perfecta para hacer eso. Veamos cómo.

// Función para buscar info de Github de un usuario.
const buscarInfoGithub = async (url) => {
  console.log(`Buscando ${url}`)
  const infoGithub = await axios(url) // Llamada de API para obtener info del usuario de Github.
  return {
    name: infoGithub.data.name,
    bio: infoGithub.data.bio,
    repos: infoGithub.data.public_repos
  }
}

// Itera todos los usuarios y regresa su info de Github.
const buscarInfoUsuario = async (nombres) => {
  const peticiones = nombres.map((nombre) => {
    const url = `https://api.github.com/users/${nombre}`
    return buscarInfoGithub(url) // Función asíncrona que busca la info del usuario.
     .then((a) => {
      return a // Regresa la info del usuario.
      })
  })
  return Promise.all(peticiones) // Esperando que todas las peticiones se resuelvan.
}


buscarInfoUsuario(['sindresorhus', 'yyx990803', 'gaearon'])
 .then(a => console.log(JSON.stringify(a)))

/*
Salida:
[{
  "name": "Sindre Sorhus",
  "bio": "Full-Time Open-Sourcerer ·· Maker ·· Into Swift and Node.js ",
  "repos": 996
}, {
  "name": "Evan You",
  "bio": "Creator of @vuejs, previously @meteor & @google",
  "repos": 151
}, {
  "name": "Dan Abramov",
  "bio": "Working on @reactjs. Co-author of Redux and Create React App. Building tools for humans.",
  "repos": 232
}]
*/

Para concluir, Promise.all es la mejor manera de agregar un grupo de promesas a una sola promesa. Este es una de las formas de lograr concurrencia en JavaScript.

Espero que te haya gustado este artículo. Si te gustó, por favor aplaude y compártelo.

Inclusive si no te gustó, no pasa nada, lo puedes compartir de todas formas :P