javascript - promesas - ¿Hay alguna diferencia entre await Promise.all() y multiple await?
promise javascript (3)
Primera diferencia: falla rápidamente
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.
Segunda diferencia: manejo de errores
Pero al considerar el manejo de errores, DEBE usar Promise.all.
No es posible manejar correctamente los errores de las 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 manera bastante bonita de cómo usar promesas ... el código asincrónico está escrito en estilo de sincronización y podemos evitar múltiples 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 con la
Task 1 succeed! Task 2 succeed!
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 función catch para
run
y el código con la lógica catch está en devolución de llamada (
estilo asíncrono
).
No necesitamos manejar errores dentro de la función de
run
porque la función asíncrona lo hace automáticamente: el rechazo de la promesa de la función de
task
provoca el rechazo de la función de
run
.
Para evitar la devolución de llamada, podemos usar el
estilo de sincronización
(async / await + try / catch)
try { await run(); } catch(err) { }
try { await run(); } catch(err) { }
pero en este ejemplo no es posible porque no podemos usar el
await
en el hilo principal; solo se puede usar en la función asíncrona (es lógico porque nadie quiere bloquear el hilo principal).
Para probar si el manejo funciona en
estilo de sincronización,
podemos llamar a la función de
run
desde otra función asincrónica o usar IIFE (Expresión de función invocada inmediatamente):
(async function() { try { await run(); } catch(err) { console.log(''Caught error'', err); }; })();
.
Esta es solo una forma 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 la
run
es asíncrona
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.
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 tareas por separado y también el 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 utilicé negativeScenario = true para ambas tareas para una mejor demostración de lo que sucede (
throw err
se utiliza para disparar el error final)
¿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];
A los fines 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.
Llamar
res
inicia el temporizador.
El uso de
Promise.all
para esperar un puñado de demoras se resolverá una vez que todas las demoras hayan terminado, pero recuerde que se ejecutan al mismo tiempo:
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
async function example() {
const start = Date.now()
let i = 0
function res(n) {
const id = ++i
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
console.log(`res #${id} called after ${n} milliseconds`, Date.now() - start)
}, n)
})
}
const data = await Promise.all([res(3000), res(2000), res(1000)])
console.log(`Promise.all finished`, Date.now() - start)
}
example()
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 "falla rápida"
:
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
async function example() {
const start = Date.now()
let i = 0
function res(n) {
const id = ++i
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
console.log(`res #${id} called after ${n} milliseconds`, Date.now() - start)
}, n)
})
}
function rej(n) {
const id = ++i
return new Promise((resolve, reject) => {
setTimeout(() => {
reject()
console.log(`rej #${id} called after ${n} milliseconds`, Date.now() - start)
}, n)
})
}
try {
const data = await Promise.all([res(3000), res(2000), rej(1000)])
} catch (error) {
console.log(`Promise.all finished`, Date.now() - start)
}
}
example()
Si utiliza
async-await
lugar, tendrá que esperar a que cada promesa se resuelva secuencialmente, lo que puede no ser tan eficiente:
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
async function example() {
const start = Date.now()
let i = 0
function res(n) {
const id = ++i
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
console.log(`res #${id} called after ${n} milliseconds`, Date.now() - start)
}, n)
})
}
function rej(n) {
const id = ++i
return new Promise((resolve, reject) => {
setTimeout(() => {
reject()
console.log(`rej #${id} called after ${n} milliseconds`, Date.now() - start)
}, n)
})
}
try {
const delay1 = res(3000)
const delay2 = res(2000)
const delay3 = rej(1000)
const data1 = await delay1
const data2 = await delay2
const data3 = await delay3
} catch (error) {
console.log(`await finished`, Date.now() - start)
}
}
example()
Puedes comprobarlo por ti mismo.
En este
fiddle
, realicé una prueba para demostrar la naturaleza de bloqueo de la
await
, a diferencia de
Promise.all
cual iniciará todas las promesas y mientras una espera, continuará con las demás.