Primera diferencia: falla rápido
Estoy de acuerdo con la respuesta de @ zzzzBov, pero la ventaja de "fallar rápido" de Promise.all no es solo la única diferencia. Algunos usuarios en los comentarios preguntan por qué usar Promise.all cuando solo es más rápido en un escenario negativo (cuando falla una tarea). Y pregunto por qué no. Si tengo dos tareas paralelas asíncronas independientes y la primera se resuelve en mucho tiempo, pero la segunda se rechaza en muy poco tiempo, ¿por qué dejar al usuario esperando el mensaje de error "mucho tiempo" en lugar de "muy poco tiempo"? En aplicaciones de la vida real debemos considerar un escenario negativo. Pero está bien: en esta primera diferencia, puede decidir qué alternativa usar Promise.all vs. multiple wait.
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 desencadenadas con espera múltiple. En un escenario negativo, siempre terminará con UnhandledPromiseRejectionWarning
y PromiseRejectionHandledWarning
aunque use try / catch en cualquier lugar. Es por eso que 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 no es una buena práctica. Encontré muchos ejemplos en Internet que no consideran el manejo de errores para dos o más tareas paralelas asíncronas independientes o lo consideran de manera incorrecta, solo usando try / catch y esperando que detecte errores. Es casi imposible encontrar buenas prácticas. Por eso estoy escribiendo esta respuesta.
Resumen
Nunca use el modo de espera múltiple para dos o más tareas paralelas asíncronas independientes porque no podrá manejar los errores en serio. Utilice siempre Promise.all () para este caso de uso.
Async / await no reemplaza a Promises. Es una forma bastante bonita de usar promesas ... el código asincrónico está escrito en estilo de sincronización y podemos evitar múltiples then
promesas.
Algunas personas dicen que usando Promise.all () no podemos manejar los errores de las tareas por separado, sino solo el error de la primera promesa rechazada (sí, algunos casos de uso pueden requerir un manejo separado, por ejemplo, para el registro). No es un problema; consulte el título "Adición" a continuación.
Ejemplos
Considere esta tarea asíncrona ...
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 un escenario positivo, no hay diferencia entre Promise.all y múltiples en espera. Ambos ejemplos terminan Task 1 succeed! Task 2 succeed!
después de 5 segundos.
// Promise.all alternative
const run = async function() {
// tasks run immediate 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 immediate 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!
Cuando la primera tarea demora 10 segundos en un escenario positivo y la tarea de segundos demora 5 segundos en un escenario negativo, 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 cuando utilizamos múltiples esperas en paralelo. Por supuesto, para evitar errores, debemos manejarlo. Intentemos...
// 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 con éxito el error, necesitamos agregar solo una captura para run
funcionar y el código con lógica de captura está en devolución de llamada ( estilo asíncrono ). No necesitamos manejar errores dentro de la run
función porque la función asíncrona lo hace automáticamente: el rechazo de la promesa de promesa task
causa el rechazo de la run
función. Para evitar la 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 la función async (es lógico porque nadie quiere) bloquear el hilo principal). Para probar si el manejo funciona en estilo de sincronización, podemos llamarrun
función desde otra función o uso asíncrono IIFE (Inmediatamente Se invoca expresión de función): (async function() { try { await run(); } catch(err) { console.log('Caught error', err); }; })();
.
Esta es solo una manera correcta de cómo ejecutar dos o más tareas paralelas asíncronas y manejar errores. Debe evitar los ejemplos a continuación.
// 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 tratar de manejar 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 el 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)
... Wtf? Primero vemos que el error para la tarea 2 no se manejó y luego se detectó. Engañoso y aún lleno de errores en la consola. Inutilizable 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 que parece detectarse pero también hay errores no manejados. Capturamos el error porque run () se rechaza en línea con la palabra clave wait y se puede capturar usando try / catch al llamar a run (). También recibimos un error no controlado porque estamos llamando a la función de tarea asíncrona sincrónicamente (sin esperar palabra clave) y esta tarea se ejecuta fuera de la función run () y también falla fuera. Es similar cuando no somos capaces de manejar el error por try / catch cuando se llama a una función de sincronización de qué parte del código se ejecuta en setTimeout ... function test() { setTimeout(function() { console.log(causesError); }, 0); }; try { test(); } catch(e) { /* this will never catch error */ }
.
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)
... "solo" dos errores (falta el tercero) pero no se detecta nada.
Adición (maneje los errores de la tarea por separado y también el error de primer fallo)
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 utilicé negativeScenario = true para ambas tareas para una mejor demostración de lo que sucede ( throw err
se utiliza para disparar el error final)