¿Alguna diferencia entre await Promise.all() y multiple await?
¿Hay alguna diferencia entre:
const [result1, result2] = await Promise.all([task1(), task2()]);
y
const t1 = task1();
const t2 = task2();
const result1 = await t1;
const result2 = await t2;
y
const [t1, t2] = [task1(), task2()];
const [result1, result2] = [await t1, await t2];
Nota : esta respuesta solo cubre las diferencias de tiempo entre await
en serie y Promise.all
. Asegúrese de leer la respuesta completa de @mikep que también cubre las diferencias más importantes en el manejo de errores .
A los efectos de esta respuesta, utilizaré algunos métodos de ejemplo:
res(ms)
es una función que toma un número entero de milisegundos y devuelve una promesa que se resuelve después de tantos milisegundos.rej(ms)
es una función que toma un número entero de milisegundos y devuelve una promesa que rechaza después de tantos milisegundos.
La llamada res
inicia el cronómetro. Usar Promise.all
para esperar varios retrasos se resolverá después de que todos los retrasos hayan finalizado, pero recuerda que se ejecutan al mismo tiempo:
Ejemplo 1
const data = await Promise.all([res(3000), res(2000), res(1000)])
// ^^^^^^^^^ ^^^^^^^^^ ^^^^^^^^^
// delay 1 delay 2 delay 3
//
// ms ------1---------2---------3
// =============================O delay 1
// ===================O delay 2
// =========O delay 3
//
// =============================O Promise.all
Mostrar fragmento de código
Esto significa que Promise.all
se resolverá con los datos de las promesas internas después de 3 segundos.
Pero Promise.all
tiene un comportamiento de "fallo rápido" :
Ejemplo #2
const data = await Promise.all([res(3000), res(2000), rej(1000)])
// ^^^^^^^^^ ^^^^^^^^^ ^^^^^^^^^
// delay 1 delay 2 delay 3
//
// ms ------1---------2---------3
// =============================O delay 1
// ===================O delay 2
// =========X delay 3
//
// =========X Promise.all
Mostrar fragmento de código
Si usas async-await
en su lugar, tendrás que esperar a que cada promesa se resuelva secuencialmente, lo que puede no ser tan eficiente:
Ejemplo #3
const delay1 = res(3000)
const delay2 = res(2000)
const delay3 = rej(1000)
const data1 = await delay1
const data2 = await delay2
const data3 = await delay3
// ms ------1---------2---------3
// =============================O delay 1
// ===================O delay 2
// =========X delay 3
//
// =============================X await
Mostrar fragmento de código
Primera diferencia: falla rápido
Estoy de acuerdo con la respuesta de @zzzzBov , pero la ventaja de "fallar rápido" Promise.all
no es la única diferencia. Algunos usuarios en los comentarios han preguntado por qué Promise.all
vale la pena usarlo cuando solo es más rápido en el escenario negativo (cuando falla alguna tarea). Y pregunto ¿por qué no? Si tengo dos tareas paralelas asíncronas independientes y la primera tarda mucho en resolverse pero la segunda se rechaza en muy poco tiempo, ¿por qué dejar que el usuario espere a que finalice la llamada más larga para recibir un mensaje de error? En aplicaciones de la vida real debemos considerar el escenario negativo. Pero está bien, en esta primera diferencia puedes decidir qué alternativa usar: Promise.all
versus múltiple await
.
Segunda diferencia: manejo de errores
Pero al considerar el manejo de errores, DEBE usar Promise.all
. No es posible manejar correctamente los errores de tareas paralelas asíncronas activadas con varios await
mensajes de correo electrónico. En el escenario negativo, siempre terminarás con UnhandledPromiseRejectionWarning
y PromiseRejectionHandledWarning
, independientemente de dónde uses try/catch. Por eso Promise.all
fue diseñado. Por supuesto, alguien podría decir que podemos suprimir esos errores usando process.on('unhandledRejection', err => {})
y process.on('rejectionHandled', err => {})
, pero esto no es una buena práctica. He encontrado muchos ejemplos en Internet que no consideran el manejo de errores para dos o más tareas paralelas asíncronas independientes en absoluto, o lo consideran pero de manera incorrecta: simplemente usando try/catch y esperando que detecte errores. Es casi imposible encontrar buenas prácticas en esto.
Resumen
TL;DR: Nunca use múltiples await
para dos o más tareas paralelas asíncronas independientes, porque no podrá manejar los errores correctamente. Úselo siempre Promise.all()
para este caso de uso.
Async/ await
no reemplaza las promesas, es solo una forma bonita de usar las promesas. El código asíncrono está escrito en "estilo de sincronización" y podemos evitar múltiples then
mensajes en las promesas.
Algunas personas dicen que cuando usamos Promise.all()
no podemos manejar los errores de tareas por separado y que solo podemos manejar el error de la primera promesa rechazada (el manejo por separado puede ser útil, por ejemplo, para el registro). Esto no es un problema; consulte el título "Adición" al final de esta respuesta.
Ejemplos
Considere esta tarea asincrónica...
const task = function(taskNum, seconds, negativeScenario) {
return new Promise((resolve, reject) => {
setTimeout(_ => {
if (negativeScenario)
reject(new Error('Task ' + taskNum + ' failed!'));
else
resolve('Task ' + taskNum + ' succeed!');
}, seconds * 1000)
});
};
Cuando ejecuta tareas en el escenario positivo, no hay diferencia entre Promise.all
y múltiples await
. Ambos ejemplos terminan Task 1 succeed! Task 2 succeed!
después de 5 segundos.
// Promise.all alternative
const run = async function() {
// tasks run immediately in parallel and wait for both results
let [r1, r2] = await Promise.all([
task(1, 5, false),
task(2, 5, false)
]);
console.log(r1 + ' ' + r2);
};
run();
// at 5th sec: Task 1 succeed! Task 2 succeed!
// multiple await alternative
const run = async function() {
// tasks run immediately in parallel
let t1 = task(1, 5, false);
let t2 = task(2, 5, false);
// wait for both results
let r1 = await t1;
let r2 = await t2;
console.log(r1 + ' ' + r2);
};
run();
// at 5th sec: Task 1 succeed! Task 2 succeed!
Sin embargo, cuando la primera tarea tarda 10 segundos y tiene éxito, y la segunda tarea tarda 5 segundos pero falla, existen diferencias en los errores emitidos.
// Promise.all alternative
const run = async function() {
let [r1, r2] = await Promise.all([
task(1, 10, false),
task(2, 5, true)
]);
console.log(r1 + ' ' + r2);
};
run();
// at 5th sec: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
// multiple await alternative
const run = async function() {
let t1 = task(1, 10, false);
let t2 = task(2, 5, true);
let r1 = await t1;
let r2 = await t2;
console.log(r1 + ' ' + r2);
};
run();
// at 5th sec: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
// at 10th sec: PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)
// at 10th sec: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
Ya deberíamos notar aquí que estamos haciendo algo mal al usar varios await
s en paralelo. Intentemos manejar los errores:
// Promise.all alternative
const run = async function() {
let [r1, r2] = await Promise.all([
task(1, 10, false),
task(2, 5, true)
]);
console.log(r1 + ' ' + r2);
};
run().catch(err => { console.log('Caught error', err); });
// at 5th sec: Caught error Error: Task 2 failed!
Como puede ver, para manejar los errores con éxito, necesitamos agregar solo un catch a la run
función y agregar código con lógica catch en la devolución de llamada. No necesitamos manejar errores dentro de la run
función porque las funciones asíncronas lo hacen automáticamente: el rechazo de la promesa de la task
función provoca el rechazo de la run
función.
Para evitar una devolución de llamada, podemos usar el "estilo de sincronización" (async/ await
+ try/ catch)
try { await run(); } catch(err) { }
, pero en este ejemplo no es posible porque no podemos usarlo await
en el hilo principal; solo se puede usar en funciones asíncronas (porque nadie quiere para bloquear el hilo principal). Para probar si el manejo funciona en "estilo de sincronización", podemos llamar a la run
función desde otra función asíncrona o usar una IIFE (Expresión de función invocada inmediatamente: MDN ):
(async function() {
try {
await run();
} catch(err) {
console.log('Caught error', err);
}
})();
Esta es la única forma correcta de ejecutar dos o más tareas paralelas asíncronas y manejar errores. Debes evitar los ejemplos siguientes.
Malos ejemplos
// multiple await alternative
const run = async function() {
let t1 = task(1, 10, false);
let t2 = task(2, 5, true);
let r1 = await t1;
let r2 = await t2;
console.log(r1 + ' ' + r2);
};
Podemos intentar manejar los errores en el código anterior de varias maneras...
try { run(); } catch(err) { console.log('Caught error', err); };
// at 5th sec: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
// at 10th sec: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
// at 10th sec: PromiseRejectionHandledWarning: Promise rejection was handled
... no se detectó nada porque maneja código de sincronización pero run
es asíncrono.
run().catch(err => { console.log('Caught error', err); });
// at 5th sec: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
// at 10th sec: Caught error Error: Task 2 failed!
// at 10th sec: PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)
... ¿eh? En primer lugar, vemos que el error de la tarea 2 no se manejó y luego se detectó. Engañoso y todavía lleno de errores en la consola, todavía no se puede utilizar de esta manera.
(async function() { try { await run(); } catch(err) { console.log('Caught error', err); }; })();
// at 5th sec: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
// at 10th sec: Caught error Error: Task 2 failed!
// at 10th sec: PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)
... lo mismo de arriba. El usuario @Qwerty en su respuesta eliminada preguntó sobre este extraño comportamiento en el que parece detectarse un error pero tampoco se controla. Detectamos el error porque run()
se rechaza en la línea con la await
palabra clave y se puede detectar usando try/catch al llamar run()
. También obtenemos un error no controlado porque estamos llamando a una función de tarea asíncrona sincrónicamente (sin la await
palabra clave) y esta tarea se ejecuta y falla fuera de la run()
función.
Es similar a cuando no podemos manejar errores mediante try/catch al llamar a alguna función de sincronización que llama a setTimeout:
function test() {
setTimeout(function() {
console.log(causesError);
}, 0);
};
try {
test();
} catch(e) {
/* this will never catch error */
}`.
Otro mal ejemplo:
const run = async function() {
try {
let t1 = task(1, 10, false);
let t2 = task(2, 5, true);
let r1 = await t1;
let r2 = await t2;
}
catch (err) {
return new Error(err);
}
console.log(r1 + ' ' + r2);
};
run().catch(err => { console.log('Caught error', err); });
// at 5th sec: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
// at 10th sec: PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)
... "sólo" dos errores (falta el tercero) pero no se detecta nada.
Adición (manejo de errores de tareas separadas y también error de primer error)
const run = async function() {
let [r1, r2] = await Promise.all([
task(1, 10, true).catch(err => { console.log('Task 1 failed!'); throw err; }),
task(2, 5, true).catch(err => { console.log('Task 2 failed!'); throw err; })
]);
console.log(r1 + ' ' + r2);
};
run().catch(err => { console.log('Run failed (does not matter which task)!'); });
// at 5th sec: Task 2 failed!
// at 5th sec: Run failed (does not matter which task)!
// at 10th sec: Task 1 failed!
... tenga en cuenta que en este ejemplo rechacé ambas tareas para demostrar mejor lo que sucede ( throw err
se usa para generar el error final).
Generalmente, el uso Promise.all()
de ejecuciones solicita "async" en paralelo. El uso await
puede ejecutarse en paralelo O ser un bloqueo de "sincronización".
Las funciones test1 y test2 a continuación muestran cómo await
se puede ejecutar async o sync.
test3 muestra Promise.all()
que es asíncrono.
jsfiddle con resultados cronometrados : abra la consola del navegador para ver los resultados de la prueba
Comportamiento de sincronización . NO se ejecuta en paralelo, tarda ~ 1800 ms :
const test1 = async () => {
const delay1 = await Promise.delay(600); //runs 1st
const delay2 = await Promise.delay(600); //waits 600 for delay1 to run
const delay3 = await Promise.delay(600); //waits 600 more for delay2 to run
};
Comportamiento asíncrono . Se ejecuta en paralelo, tarda ~ 600 ms :
const test2 = async () => {
const delay1 = Promise.delay(600);
const delay2 = Promise.delay(600);
const delay3 = Promise.delay(600);
const data1 = await delay1;
const data2 = await delay2;
const data3 = await delay3; //runs all delays simultaneously
}
Comportamiento asíncrono . Se ejecuta en paralelo, tarda ~ 600 ms :
const test3 = async () => {
await Promise.all([
Promise.delay(600),
Promise.delay(600),
Promise.delay(600)]); //runs all delays simultaneously
};
TLDR; Si lo está utilizando, Promise.all()
también producirá un "fallo rápido": dejará de ejecutarse en el momento del primer fallo de cualquiera de las funciones incluidas.
Puedes comprobarlo por ti mismo.
En este violín , realicé una prueba para demostrar la naturaleza de bloqueo de await
, a diferencia de Promise.all
lo cual iniciará todas las promesas y mientras una espera continuará con las demás.