Alguma diferença entre aguardar Promise.all () e vários aguardar?

181

Existe alguma diferença entre:

const [result1, result2] = await Promise.all([task1(), task2()]);

e

const t1 = task1();
const t2 = task2();

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

e

const [t1, t2] = [task1(), task2()];
const [result1, result2] = [await t1, await t2];
Escondido
fonte

Respostas:

209

Nota :

Esta resposta cobre apenas as diferenças de tempo entre as awaitséries e Promise.all. Leia a resposta abrangente do @ mikep, que também cobre as diferenças mais importantes no tratamento de erros .


Para os fins desta resposta, usarei alguns métodos de exemplo:

  • res(ms) é uma função que leva um número inteiro de milissegundos e retorna uma promessa que é resolvida após muitos milissegundos.
  • rej(ms) é uma função que leva um número inteiro de milissegundos e retorna uma promessa que rejeita depois de muitos milissegundos.

A chamada resinicia o temporizador. UsandoPromise.all para aguardar alguns atrasos será resolvido após o término de todos os atrasos, mas lembre-se de que eles são executados ao mesmo tempo:

Exemplo 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

Isso significa que Promise.all será resolvido com os dados das promessas internas após 3 segundos.

Mas, Promise.alltem um comportamento "falhar rápido" :

Exemplo 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

Se você usar async-await, terá que aguardar que cada promessa seja resolvida sequencialmente, o que pode não ser tão eficiente:

Exemplo 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
fonte
4
Então, basicamente, a diferença é apenas o recurso "falhar rápido" do Promise.all?
Matthew
4
@mclzc No exemplo # 3, a execução adicional do código é interrompida até que o atraso1 seja resolvido. É ainda no texto "Se você usar assíncrono esperam por vez, você vai ter que esperar para cada promessa para resolver sequencialmente"
haggis
1
@ Qback, há um trecho de código ao vivo que demonstra o comportamento. Considere executá-lo e reler o código. Você não é a primeira pessoa a entender mal como se comporta a sequência de promessas. O erro que você cometeu na sua demonstração é que você não está iniciando suas promessas ao mesmo tempo.
zzzzBov
1
@zzzzBov Você está certo. Você está começando no mesmo tempo. Desculpe, eu vim para esta pergunta por outro motivo e eu ignorei isso.
QBack
2
" pode não ser tão eficiente " - e, mais importante, causar unhandledrejectionerros. Você nunca vai querer usar isso. Adicione isso à sua resposta.
Bergi 14/08/19
87

Primeira diferença - falha rapidamente

Concordo com a resposta de @ zzzzBov, mas a vantagem "falhar rápido" do Promise.all não é apenas a única diferença. Alguns usuários nos comentários perguntam por que usar Promise.all quando é apenas mais rápido no cenário negativo (quando alguma tarefa falha). E eu pergunto por que não? Se eu tenho duas tarefas paralelas assíncronas independentes e a primeira é resolvida em muito tempo, mas a segunda é rejeitada em muito pouco tempo, por que deixar o usuário esperar pela mensagem de erro "tempo muito longo" em vez de "tempo muito curto"? Em aplicações da vida real, devemos considerar um cenário negativo. Mas tudo bem - nesta primeira diferença, você pode decidir qual alternativa usar Promise.all vs. multiple aguardam.

Segunda diferença - tratamento de erros

Mas, ao considerar o tratamento de erros, DEVE usar o Promise.all. Não é possível manipular corretamente erros de tarefas paralelas assíncronas acionadas com espera múltipla. No cenário negativo, você sempre terminará com UnhandledPromiseRejectionWarninge, PromiseRejectionHandledWarningembora use o try / catch em qualquer lugar. É por isso que Promise.all foi projetado. É claro que alguém poderia dizer que podemos suprimir que os erros usando process.on('unhandledRejection', err => {})e process.on('rejectionHandled', err => {}), mas não é uma boa prática. Encontrei muitos exemplos na internet que não consideram o tratamento de erros para duas ou mais tarefas paralelas assíncronas independentes ou o consideram de maneira errada - basta usar try / catch e esperar que ele consiga detectar erros. É quase impossível encontrar boas práticas. É por isso que estou escrevendo esta resposta.

Resumo

Nunca use múltiplo aguarde por duas ou mais tarefas paralelas assíncronas independentes, pois você não poderá lidar com erros seriamente. Sempre use Promise.all () para este caso de uso. Async / waitit não substitui Promessas. É muito bonito como usar promessas ... código assíncrono é escrito em estilo de sincronização e podemos evitar váriosthen promessas.

Algumas pessoas dizem que usando Promise.all () não podemos lidar com erros de tarefas separadamente, mas apenas com erros da primeira promessa rejeitada (sim, alguns casos de uso podem exigir tratamento separado, por exemplo, para registro). Não é problema - consulte o título "Adição" abaixo.

Exemplos

Considere esta tarefa assí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)
  });
};

Quando você executa tarefas em um cenário positivo, não há diferença entre Promise.all e vários aguardam. Ambos os exemplos terminam Task 1 succeed! Task 2 succeed!após 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!

Quando a primeira tarefa leva 10 segundos no cenário positivo e a tarefa segundos leva 5 segundos no cenário negativo, existem diferenças nos erros 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!

Já devemos notar aqui que estamos fazendo algo errado ao usar vários esperam em paralelo. Obviamente, para evitar erros, devemos lidar com isso! Vamos tentar...


// 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 você pode ver para tratar com êxito do erro, precisamos adicionar apenas uma captura à runfunção e o código com lógica de captura está no retorno de chamada ( estilo assíncrono ). Não precisamos manipular erros dentro da runfunção porque a função assíncrona faz automaticamente - prometer rejeição da taskfunção causa rejeição da runfunção. Para evitar retorno de chamada, podemos usar o estilo de sincronização (assíncrono / aguardar + tentar / capturar), try { await run(); } catch(err) { }mas neste exemplo não é possível porque não podemos usar awaitno encadeamento principal - ele pode ser usado apenas na função assíncrona (é lógico, porque ninguém quer bloquear a linha principal). Para testar se o tratamento funciona no estilo de sincronização , podemos chamarrunfunção de uma outra função assíncrono ou uso IIFE (Imediatamente Invoked Função Expressão): (async function() { try { await run(); } catch(err) { console.log('Caught error', err); }; })();.

Essa é apenas uma maneira correta de executar duas ou mais tarefas paralelas assíncronas e manipular erros. Você deve evitar exemplos abaixo.


// 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 tentar manipular o código acima de várias maneiras ...

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 

... nada foi pego porque ele lida com código de sincronização, mas runé assí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? Vimos, em primeiro lugar, que o erro da tarefa 2 não foi tratado e, posteriormente, foi capturado. Enganador e ainda cheio de erros no console. Inutilizável dessa maneira.

(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)

... O mesmo que acima. O usuário @Qwerty, em sua resposta excluída, perguntou sobre esse comportamento estranho que parece ter sido detectado, mas também há erros não tratados. Nós capturamos o erro porque run () é rejeitado on-line com a palavra-chave wait e pode ser capturado usando try / catch ao chamar run (). Também recebemos erro não tratado porque estamos chamando a função de tarefa assíncrona de forma síncrona (sem palavra-chave aguardada) e essa tarefa é executada fora da função run () e também falha fora. É semelhante quando não somos capazes de lidar com erro try / catch ao chamar alguma função sync qual parte do código é executado em 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)

... "apenas" dois erros (o terceiro está faltando), mas nada foi detectado.


Adição (manipule os erros da tarefa separadamente e também o erro de primeira falha)

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!

... observe que neste exemplo eu usei negativeScenario = true para ambas as tarefas para melhor demonstração do que acontece ( throw erré usado para disparar o erro final)

mikep
fonte
14
esta resposta é melhor do que a resposta aceita porque a resposta atualmente aceita perde o tópico muito importante do tratamento de erros
chrishiestand
8

Geralmente, o uso de Promise.all()solicitações de execução "assíncronas" em paralelo. O uso awaitpode ser executado em paralelo OU ser "sincronizado".

As funções test1 e test2 abaixo mostram como awaitexecutar async ou sync.

test3 mostra Promise.all()que é assíncrono.

jsfiddle com resultados programados - abra o console do navegador para ver os resultados dos testes

Comportamento de sincronização . NÃO roda em paralelo, leva ~ 1800ms :

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
};

Comportamento assíncrono . É executado em paralelo, leva ~ 600ms :

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
}

Comportamento assíncrono . Funciona em paralelo, leva ~ 600ms :

const test3 = async () => {
  await Promise.all([
  Promise.delay(600), 
  Promise.delay(600), 
  Promise.delay(600)]); //runs all delays simultaneously
};

TLDR; Se você estiver usando, Promise.all()ele também "falhará rapidamente" - pare de executar no momento da primeira falha de qualquer uma das funções incluídas.

GavinBelson
fonte
1
Onde posso obter uma explicação detalhada do que acontece nos trechos 1 e 2? Estou tão surpreso que eles tenham uma maneira diferente de funcionar, pois eu esperava que os comportamentos fossem os mesmos.
Gregordy 4/03
2
@ Gregordy sim, é surpreendente. Publiquei esta resposta para salvar novos codificadores para async algumas dores de cabeça. É tudo sobre quando o JS avalia a espera, é por isso que você atribui variáveis. Leitura assíncrona em profundidade: blog.bitsrc.io/…
GavinBelson
7

Você pode verificar por si mesmo.

Neste violino , realizei um teste para demonstrar a natureza do bloqueio await, ao contrário do Promise.allque iniciará todas as promessas e enquanto um aguarda, ele continuará com os outros.

zpr
fonte
6
Na verdade, seu violino não aborda a pergunta dele. Há uma diferença entre ligar t1 = task1(); t2 = task2()e depois usar awaitpara os dois, result1 = await t1; result2 = await t2;como na pergunta dele, em oposição ao que você está testando e que está usando awaitna chamada original result1 = await task1(); result2 = await task2();. O código em sua pergunta inicia todas as promessas de uma só vez. A diferença, como mostra a resposta, é que as falhas serão relatadas mais rapidamente com o Promise.allcaminho.
BryanGrezeszak
Sua resposta está fora do tópico, como @BryanGrezeszak comentou. Você deve excluí-lo para evitar usuários enganosos.
Mikep 21/01/19
0

No caso de aguardar Promise.all ([task1 (), task2 ()]); "task1 ()" e "task2 ()" serão executados em paralelo e aguardarão até que ambas as promessas sejam concluídas (resolvidas ou rejeitadas). Considerando que, no caso de

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

T2 só será executado depois que T1 terminar a execução (tiver sido resolvido ou rejeitado). T1 e t2 não serão executados em paralelo.

Waleed Naveed
fonte