¿Hay alguna diferencia entre await Promise.all () y multiple await?

181

¿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];
Oculto
fuente

Respuestas:

209

Nota :

Esta respuesta solo cubre las diferencias de tiempo entre awaiten serie y Promise.all. Asegúrese de leer la respuesta integral de @ mikep que también cubre las diferencias más importantes en el manejo de errores .


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 resinicia el temporizador. Usar Promise.allpara esperar un puñado de demoras se resolverá una vez que todas las demoras hayan terminado, pero recuerde 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

Esto significa que Promise.allse resolverá con los datos de las promesas internas después de 3 segundos.

Pero Promise.alltiene un comportamiento de "falla rápida" :

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

Si lo utiliza async-await, deberá 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

zzzzBov
fuente
44
Entonces, básicamente, la diferencia es solo la característica de "falla rápida" de Promise.all?
Mateo
44
@mclzc En el ejemplo # 3, la ejecución de código adicional se detiene hasta que se resuelva el retraso1. Incluso está en el texto "Si utiliza async-wait en su lugar, tendrá que esperar a que cada promesa se resuelva secuencialmente"
haggis
1
@Qback, hay un fragmento de código en vivo que demuestra el comportamiento. Considere ejecutarlo y releer el código. No eres la primera persona en entender mal cómo se comporta la secuencia de promesas. El error que cometió en su demostración es que no está comenzando sus promesas al mismo tiempo.
zzzzBov
1
@zzzzBov Tienes razón. Lo estás comenzando al mismo tiempo. Lo siento, llegué a esta pregunta por otra razón y la pasé por alto.
Qback
2
" Puede que no sea tan eficiente " y, lo que es más importante, puede causar unhandledrejectionerrores. Nunca querrás usar esto. Por favor agregue esto a su respuesta.
Bergi
87

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 UnhandledPromiseRejectionWarningy PromiseRejectionHandledWarningaunque 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 thenpromesas.

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 runfuncionar 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 runfunción porque la función asíncrona lo hace automáticamente: el rechazo de la promesa de promesa taskcausa el rechazo de la runfunció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 awaiten 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 llamarrunfunció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 runes 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 errse utiliza para disparar el error final)

mikep
fuente
14
esta respuesta es mejor que la respuesta aceptada porque la respuesta actualmente aceptada pierde el tema muy importante del manejo de errores
chrishiestand
8

En general, el uso de Promise.all()solicitudes de ejecución "asíncrono" en paralelo. El uso awaitpuede ejecutarse en paralelo O ser un bloqueo de "sincronización".

Las funciones test1 y test2 a continuación muestran cómo se awaitpuede 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 "fallará rápidamente": dejará de ejecutarse en el momento del primer fallo de cualquiera de las funciones incluidas.

GavinBelson
fuente
1
¿Dónde puedo obtener una explicación detallada de lo que sucede debajo del capó en los fragmentos 1 y 2? Estoy tan sorprendido de que estos tengan una forma diferente de correr, ya que esperaba que los comportamientos fueran los mismos.
Gregordy
2
@ Gregoryy sí, es sorprendente. Publiqué esta respuesta para salvar a los codificadores nuevos para sincronizar algunos dolores de cabeza. Se trata de cuando JS evalúa la espera, es por eso que la forma de asignar variables es importante. Lectura asincrónica
GavinBelson
7

Puedes comprobarlo por ti mismo.

En este violín , realicé una prueba para demostrar la naturaleza de bloqueo await, en contraposición a la Promise.allcual comenzará todas las promesas y mientras una espera, continuará con las otras.

zpr
fuente
66
En realidad, tu violín no responde a su pregunta. Hay una diferencia entre llamar t1 = task1(); t2 = task2()y luego usar awaitpara ambos result1 = await t1; result2 = await t2;como en su pregunta, en lugar de lo que está probando, que está usando awaiten la llamada original result1 = await task1(); result2 = await task2();. El código en su pregunta comienza todas las promesas a la vez. La diferencia, como muestra la respuesta, es que las fallas se informarán más rápido con el Promise.allcamino.
BryanGrezeszak
Su respuesta está fuera de tema, como comentó @BryanGrezeszak. Debería eliminarlo para evitar engañar a los usuarios.
mikep
0

En caso de esperar Promise.all ([tarea1 (), tarea2 ()]); "task1 ()" y "task2 ()" se ejecutarán en paralelo y esperarán hasta que se completen ambas promesas (resueltas o rechazadas). Considerando que en caso de

const result1 = await t1;
const result2 = await t2;

t2 solo se ejecutará después de que t1 haya finalizado la ejecución (se haya resuelto o rechazado). Tanto t1 como t2 no se ejecutarán en paralelo.

Waleed Naveed
fuente