Ahora que sabemos ¿Qué son las promesas?, para qué y como se usan, podemos profundizar y aprender más sobre la API Promise nativa de Javascript, mediante la cuál podemos realizar operaciones con grupos de promesas, tanto independientes como dependientes entre sí.
Por norma general, las tareas asíncronas no sabemos cuanto tardarán en responder y/o procesarse, por lo que muchas veces el orden en que se resuelven no será el mismo. Esto en algunos casos no nos importará, pero en otros sí, por lo que hay que tenerlo en cuenta.
Esperar varias promesas
Imaginemos el siguiente supuesto, donde hacemos múltiples tareas asíncronas y queremos realizar una tarea cuando las tres estén resueltas, por lo que necesitamos esperar a tener las tres promesas cumplidas.
Quizás nuestra primera aproximación sería la siguiente: recorrer el array de promesas promises
con un .map()
, de modo que creamos un nuevo array derivado con las promesas ya resueltas, esperando por ellas con un await
:
const p1 = fetch("/robots.txt");
const p2 = fetch("/theme.css");
const p3 = fetch("/index.js");
const promises = [p1, p2, p3];
const responses = promises.map(async (promise) => {
return await promise
});
El planteamiento es correcto, sin embargo, el problema es que la función .map()
u otras array functions no permiten el uso de operaciones asíncronas en su interior, ya que trabajan de forma síncrona.
Si lo hacemos, veremos como la constante
responses
en lugar de un array de respuestas como probablemente esperábamos, tendrá un array de promesas. Es decir, tendremos exactamente lo mismo que teníamos inicialmente enpromises
.
Para solucionar este problema, podemos utilizar el objeto Promise
de Javascript, que incorpora varios métodos estáticos que podemos utilizar en nuestro código. Todos devuelven una promesa (son asíncronos) y son los que veremos a continuación:
Métodos | Disponible | Descripción |
---|---|---|
Promise.all( list) | Acepta sólo si todas las promesas del | |
Promise.allSettled( list) | Acepta sólo si todas las promesas del | |
Promise.any( list) | Acepta con el valor de la primera promesa del | |
Promise.race( list) | Acepta o rechaza según la primera promesa del | |
Promise.resolve( value) | Devuelve una promesa cumplida directamente con el | |
Promise.reject( value) | Devuelve una promesa rechazada directamente con el |
En los siguientes ejemplos, vamos a utilizar la función fetch()
para realizar varias peticiones y descargar varios archivos diferentes que necesitaremos para nuestras tareas.
El método Promise.all()
El método Promise.all()
funciona como un «todo o nada»: le pasas un grupo de varias promesas. El Promise.all()
te devolverá una promesa que se cumplirá cuando todas las promesas del grupo se cumplan. Si alguna de ellas se rechaza, la promesa de Promise.all()
también lo hará.
Observa este ejemplo:
const p1 = fetch("/robots.txt");
const p2 = fetch("/index.css");
const p3 = fetch("/index.js");
const promises = [p1, p2, p3];
// Utilizando async/await
const responses = await Promise.all(promises);
const codes = responses.map(response => response.status);
console.log(codes); // [200, 200, 200]
- 1️⃣ Realizamos 3
fetch()
, donde cada uno devuelve una promesa. - 2️⃣ Almacenamos esas 3 promesas en un
promises
. - 3️⃣ Al hacer un
Promise.all(promises)
devolvemos una nueva promesa. - 4️⃣ Dicha promesa se cumplirá, si todas las que pasamos en el array se cumplen invidiualmente.
- 5️⃣ En el caso de que alguna de las 3 se rechace, el
Promise.all()
la promesa se rechaza.
También se podría realizar utilizando .then()
en lugar de async/await
:
// Utilizando .then
Promise.all(promises)
.then(responses => {
const codes = responses.map(response => response.status);
console.log(codes); // [200, 200, 200]
});
El método Promise.allSettled()
El método Promise.allSettled()
funciona como un «todas procesadas»: devuelve una promesa que se cumple cuando todas las promesas del
const p1 = fetch("/robots.txt");
const p2 = fetch("https://google.com/index.css");
const p3 = fetch("/index.js");
const promises = [p1, p2, p3];
const results = await Promise.allSettled(promises);
const objects = results.map(result => result);
console.log(objects);
Esta operación nos devuelve un
- La propiedad
status
, donde nos indica si cada promesa individual ha sido cumplida o rechazada - La propiedad
value
, con los valores devueltos por la promesa si se cumple. - La propiedad
reason
, con la razón del rechazo de la promesa si no se cumple.
En este caso, obtendremos que la primera y última promesa se resuelven (fulfilled), mientras que la segunda nos da un error de CORS y se rechaza (rejected).
El método Promise.any()
El método Promise.any()
funciona como «la primera que se cumpla»: Devuelve una promesa con el valor de la primera promesa individual del
const p1 = fetch("/robots.txt");
const p2 = fetch("/index.css");
const p3 = fetch("/index.js");
const promises = [p1, p2, p3];
const response = await Promise.any(promises);
console.log(response);
Como vemos, Promise.any()
devolverá una respuesta de la primera promesa cumplida.
El método Promise.race()
El método Promise.race()
funciona como una «la primera que se procese»: la primera promesa del Promise.race()
. Si se cumple, devuelve una promesa cumplida, en caso negativo, devuelve una rechazada.
const p1 = fetch("/robots.txt");
const p2 = fetch("/index.css");
const p3 = fetch("/index.js");
const promises = [p1, p2, p3];
const response = await Promise.race([p1, p2, p3]);
console.log(response);
De forma muy similar a la anterior, Promise.race()
devolverá la promesa que se resuelva primero, ya sea cumpliéndose o rechazándose.
Promesas estáticas
Mediante los métodos estáticos Promise.resolve()
y Promise.reject()
podemos devolver una promesa cumplida o rechazada respectivamente sin necesidad de crear una promesa con new Promise()
, algo que podría ser interesante o cómodo en algunos casos.
Observa que la siguiente función doTask()
no es asíncrona:
const doTask = () => {
const number = 1 + Math.floor(Math.random() * 6);
const isEven = number % 2 === 0;
return isEven ? Promise.resolve(number) : Promise.reject(number);
}
En este caso, generamos un número aleatorio y se devuelve una promesa. Cuando el número generado es par se cumple la promesa, cuando es impar, se rechaza. Sin embargo, ten en cuenta que el problema en este caso es que la promesa no «envuelve» toda la función, por lo que si la tarea tardase algún tiempo en generar el número, no podríamos utilizar el .then()
para consumir la promesa.
Estas funciones estáticas se suelen utilizar en muy pocos casos, para mantener cierta compatibilidad en funciones que se espera que devuelvan una promesa.