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

Resuelto Hidden asked hace 7 años • 6 respuestas

¿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];
Hidden avatar Jul 24 '17 22:07 Hidden
Aceptado

Nota : esta respuesta solo cubre las diferencias de tiempo entre awaiten 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 resinicia el cronómetro. Usar Promise.allpara 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.allse resolverá con los datos de las promesas internas después de 3 segundos.

Pero Promise.alltiene 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-awaiten 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

zzzzBov avatar Jul 24 '2017 17:07 zzzzBov

Primera diferencia: falla rápido

Estoy de acuerdo con la respuesta de @zzzzBov , pero la ventaja de "fallar rápido" Promise.allno es la única diferencia. Algunos usuarios en los comentarios han preguntado por qué Promise.allvale 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.allversus 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 awaitmensajes de correo electrónico. En el escenario negativo, siempre terminarás con UnhandledPromiseRejectionWarningy PromiseRejectionHandledWarning, independientemente de dónde uses try/catch. Por eso Promise.allfue 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 awaitpara 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/ awaitno 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 thenmensajes 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.ally 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 awaits 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 runfunción y agregar código con lógica catch en la devolución de llamada. No necesitamos manejar errores dentro de la runfunción porque las funciones asíncronas lo hacen automáticamente: el rechazo de la promesa de la taskfunción provoca el rechazo de la runfunció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 awaiten 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 runfunció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 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)

... ¿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 awaitpalabra 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 awaitpalabra 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 errse usa para generar el error final).

mikep avatar Jan 21 '2019 14:01 mikep

Generalmente, el uso Promise.all()de ejecuciones solicita "async" 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 awaitse 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.

GavinBelson avatar Jan 05 '2020 04:01 GavinBelson

Puedes comprobarlo por ti mismo.

En este violín , realicé una prueba para demostrar la naturaleza de bloqueo de await, a diferencia de Promise.alllo cual iniciará todas las promesas y mientras una espera continuará con las demás.

zpr avatar Oct 23 '2017 17:10 zpr