Diferença entre `retornar e aguardar promessa` e` retornar promessa`

106

Dados os exemplos de código abaixo, há alguma diferença no comportamento e, em caso afirmativo, quais são essas diferenças?

return await promise

async function delay1Second() {
  return (await delay(1000));
}

return promise

async function delay1Second() {
  return delay(1000);
}

Pelo que entendi, o primeiro teria tratamento de erros dentro da função assíncrona, e os erros surgiam na promessa da função assíncrona. No entanto, o segundo exigiria um tique a menos. Isso está correto?

Este snippet é apenas uma função comum para retornar uma promessa para referência.

function delay(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}
PitaJ
fonte
3
Sim, editei minha pergunta porque você entendeu mal o meu significado e realmente não respondeu ao que eu estava pensando.
PitaJ de
1
@PitaJ: Acredito que você pretendia remover o asyncde sua segunda ( return promise) amostra.
Stephen Cleary
1
@PitaJ: Nesse caso, seu segundo exemplo retornaria uma promessa que é resolvida com uma promessa. Bastante estranho.
Stephen Cleary
5
jakearchibald.com/2017/await-vs-return-vs-return-await é um bom artigo que resume as diferenças
sanchit
2
@StephenCleary, tropecei nisso e primeiro pensei exatamente o mesmo, uma promessa que se resolve com uma promessa não faz sentido aqui. Mas, ao virar, promise.then(() => nestedPromise)achataria e "seguiria" o nestedPromise. Interessante como é diferente de tarefas aninhadas em C # onde teríamos que fazê- Unwraplo. Em uma nota lateral, parece que await somePromise chama Promise.resolve(somePromise).then, em vez de apenas somePromise.then, com algumas diferenças semânticas interessantes.
noseratio

Respostas:

152

Na maioria das vezes, não há diferença observável entre returne return await. Ambas as versões de delay1Secondtêm exatamente o mesmo comportamento observável (mas dependendo da implementação, a return awaitversão pode usar um pouco mais de memória porque um Promiseobjeto intermediário pode ser criado).

No entanto, como @PitaJ apontou, há um caso em que há uma diferença: se o returnou return awaitestiver aninhado em um bloco try- catch. Considere este exemplo

async function rejectionWithReturnAwait () {
  try {
    return await Promise.reject(new Error())
  } catch (e) {
    return 'Saved!'
  }
}

async function rejectionWithReturn () {
  try {
    return Promise.reject(new Error())
  } catch (e) {
    return 'Saved!'
  }
}

Na primeira versão, a função assíncrona aguarda a promessa rejeitada antes de retornar seu resultado, o que faz com que a rejeição se transforme em uma exceção e a catchcláusula seja alcançada; a função, portanto, retornará uma promessa resolvida para a string "Salvo!".

A segunda versão da função, no entanto, retorna a promessa rejeitada diretamente, sem aguardá-la na função assíncrona , o que significa que o catchcaso não é chamado e o chamador obtém a rejeição.

Denis Washington
fonte
Talvez também mencione que o rastreamento de pilha seria diferente (mesmo sem um try / catch)? Acho que esse é o problema que as pessoas enfrentam com mais frequência neste exemplo:]
Benjamin Gruenbaum
Eu descobri em um cenário, que usar return new Promise(function(resolve, reject) { })dentro de um for...ofloop e, em seguida, chamar resolve()dentro do loop após um pipe()não interrompe a execução do programa até que o pipe seja concluído, conforme desejado, mas o uso o await new Promise(...)faz. é a última sintaxe válida / correta? é uma 'abreviação' para return await new Promise(...)? você poderia me ajudar a entender por que o último funciona e o primeiro não? para o contexto, o cenário está em solution 02de esta resposta
user1063287
10

Como outras respostas mencionadas, é provável que haja um pequeno benefício de desempenho ao permitir que a promessa borbulhe ao devolvê-la diretamente - simplesmente porque você não precisa esperar o resultado primeiro e depois envolvê-lo com outra promessa novamente. No entanto, ninguém falou sobre a otimização da chamada final ainda.

A otimização da chamada final , ou “chamadas finais adequadas” , é uma técnica que o interpretador usa para otimizar a pilha de chamadas. Atualmente, poucos runtimes suportam ainda - embora seja tecnicamente parte do ES6 Standard - mas é possível que o suporte seja adicionado no futuro, então você pode se preparar para isso escrevendo um bom código no presente.

Resumindo, o TCO (ou PTC) otimiza a pilha de chamadas ao não abrir um novo quadro para uma função que é retornada diretamente por outra função. Em vez disso, ele reutiliza o mesmo quadro.

async function delay1Second() {
  return delay(1000);
}

Como delay()é retornado diretamente por delay1Second(), os tempos de execução que suportam PTC abrirão primeiro um quadro para delay1Second()(a função externa), mas, em vez de abrir outro quadro para delay()(a função interna), ele apenas reutilizará o mesmo quadro que foi aberto para a função externa. Isso otimiza a pilha porque pode evitar um estouro de pilha (hehe) com funções recursivas muito grandes, por exemplo fibonacci(5e+25),. Essencialmente, torna-se um loop, que é muito mais rápido.

PTC só é habilitado quando a função interna é retornada diretamente . Não é usado quando o resultado da função é alterado antes de ser retornado, por exemplo, se você tinha return (delay(1000) || null), ou return await delay(1000).

Mas, como eu disse, a maioria dos tempos de execução e navegadores ainda não oferecem suporte a PTC, então provavelmente não fará uma grande diferença agora, mas não faria mal ao preparar o seu código para o futuro.

Leia mais nesta pergunta: Node.js: existem otimizações para chamadas finais em funções assíncronas?

Chharvey
fonte
2

Esta é uma pergunta difícil de responder, porque depende na prática de como seu transpiler (provavelmente babel) realmente renderiza async/await. As coisas que são claras de qualquer maneira:

  • Ambas as implementações devem se comportar da mesma forma, embora a primeira implementação possa ter uma a menos Promisena cadeia.

  • Especialmente se você descartar o desnecessário await, a segunda versão não exigiria nenhum código extra do transpiler, enquanto a primeira requer.

Portanto, de uma perspectiva de desempenho de código e depuração, a segunda versão é preferível, embora apenas ligeiramente, enquanto a primeira versão tem um pequeno benefício de legibilidade, na medida em que indica claramente que retorna uma promessa.

Nrabinowitz
fonte
Por que as funções se comportariam da mesma forma? O primeiro retorna um valor resolvido ( undefined) e o segundo retorna a Promise.
Amit
4
@Amit as duas funções retornam uma promessa
PitaJ
Ack. É por isso que não suporto async/await- acho muito mais difícil raciocinar a respeito. @PitaJ está correto, ambas as funções retornam uma promessa.
nrabinowitz 01 de
E se eu envolvesse o corpo de ambas as funções assíncronas com um try-catch? No return promisecaso, nenhum rejectionseria pego, correto, enquanto, no return await promisecaso, seria, certo?
PitaJ de
Ambos retornam uma promessa, mas o primeiro "promete" um valor primitivo e o segundo "promete" uma promessa. Se você awaitcada um desses em algum site de chamada, o resultado será muito diferente.
Amit
0

deixo aqui algum código prático para você entender a diferença

 let x = async function () {
  return new Promise((res, rej) => {
    setTimeout(async function () {
      console.log("finished 1");
      return await new Promise((resolve, reject) => { // delete the return and you will see the difference
        setTimeout(function () {
          resolve("woo2");
          console.log("finished 2");
        }, 5000);
      });
      res("woo1");
    }, 3000);
  });
};

(async function () {
  var counter = 0;
  const a = setInterval(function () { // counter for every second, this is just to see the precision and understand the code
    if (counter == 7) {
      clearInterval(a);
    }

    console.log(counter);
    counter = counter + 1;
  }, 1000);
  console.time("time1");
  console.log("hello i starting first of all");
  await x();
  console.log("more code...");
  console.timeEnd("time1");
})();

a função "x" é apenas uma função assíncrona do que outra fucn se for deletar o retorno imprimir "mais código ..."

a variável x é apenas uma função assíncrona que por sua vez tem outra função assíncrona, no principal do código invocamos uma espera para chamar a função da variável x, quando completa segue a sequência do código, isso seria normal para "async / await", mas dentro da função x há outra função assíncrona, e esta retorna uma promessa ou retorna uma "promessa" que ficará dentro da função x, esquecendo o código principal, ou seja, não imprimirá o "console.log (" mais código .. "), por outro lado se colocarmos" esperar "ele irá aguardar cada função que for concluída e finalmente seguirá a sequência normal do código principal.

abaixo do "console.log (" finalizado 1 "exclua o" retorno ", você verá o comportamento.

Carlos Terrazas
fonte
1
Embora este código possa resolver a questão, incluir uma explicação de como e por que isso resolve o problema realmente ajudaria a melhorar a qualidade da sua postagem e provavelmente resultaria em mais votos positivos. Lembre-se de que você está respondendo às perguntas dos leitores no futuro, não apenas da pessoa que está perguntando agora. Por favor edite sua resposta para adicionar explicações e dar uma indicação do que limitações e premissas se aplicam.
Brian,
0

Aqui está um exemplo de texto datilografado que você pode executar e se convencer de que precisa de "retorno e espera"

async function  test() {
    try {
        return await throwErr();  // this is correct
        // return  throwErr();  // this will prevent inner catch to ever to be reached
    }
    catch (err) {
        console.log("inner catch is reached")
        return
    }
}

const throwErr = async  () => {
    throw("Fake error")
}


void test().then(() => {
    console.log("done")
}).catch(e => {
    console.log("outer catch is reached")
});

David Dehghan
fonte
0

Diferença perceptível: a rejeição da promessa é tratada em lugares diferentes

  • return somePromiseirá passar somePromise para o local da chamada e await algumaPromise para liquidar no local da chamada (se houver). Portanto, se alguma Promessa for rejeitada, ela não será tratada pelo bloco catch local, mas pelo bloco catch do site de chamada.

async function foo () {
  try {
    return Promise.reject();
  } catch (e) {
    console.log('IN');
  }
}

(async function main () {
  try {
    let a = await foo();
  } catch (e) {
    console.log('OUT');
  }
})();
// 'OUT'

  • return await somePromiseirá primeiro esperar alguma promessa para se estabelecer localmente. Portanto, o valor ou Exceção será primeiro tratado localmente. => Bloco local catch será executado se somePromisefor rejeitado.

async function foo () {
  try {
    return await Promise.reject();
  } catch (e) {
    console.log('IN');
  }
}

(async function main () {
  try {
    let a = await foo();
  } catch (e) {
    console.log('OUT');
  }
})();
// 'IN'

Motivo: return await Promiseespera localmente e fora, return Promiseespera apenas fora

Etapas detalhadas:

promessa de retorno

async function delay1Second() {
  return delay(1000);
}
  1. ligar delay1Second();
const result = await delay1Second();
  1. Dentro delay1Second(), a função delay(1000)retorna uma promessa imediatamente com [[PromiseStatus]]: 'pending. Vamos encerrar delayPromise.
async function delay1Second() {
  return delayPromise;
// delayPromise.[[PromiseStatus]]: 'pending'
// delayPromise.[[PromiseValue]]: undefined
}
  1. As funções assíncronas envolverão seu valor de retorno em Promise.resolve()( Origem ). Por delay1Secondser uma função assíncrona, temos:
const result = await Promise.resolve(delayPromise); 
// delayPromise.[[PromiseStatus]]: 'pending'
// delayPromise.[[PromiseValue]]: undefined
  1. Promise.resolve(delayPromise)retorna delayPromisesem fazer nada porque a entrada já é uma promessa (consulte MDN Promise.resolve ):
const result = await delayPromise; 
// delayPromise.[[PromiseStatus]]: 'pending'
// delayPromise.[[PromiseValue]]: undefined
  1. awaitespera até que o delayPromiseseja liquidado.
  • IF delayPromiseé cumprido com PromiseValue = 1:
const result = 1; 
  • ELSE delayPromiseé rejeitado:
// jump to catch block if there is any

retorno e espera promessa

async function delay1Second() {
  return await delay(1000);
}
  1. ligar delay1Second();
const result = await delay1Second();
  1. Dentro delay1Second(), a função delay(1000)retorna uma promessa imediatamente com [[PromiseStatus]]: 'pending. Vamos encerrar delayPromise.
async function delay1Second() {
  return await delayPromise;
// delayPromise.[[PromiseStatus]]: 'pending'
// delayPromise.[[PromiseValue]]: undefined
}
  1. A espera local vai esperar até delayPromiseser resolvida.
  • Caso 1 : delayPromiseé cumprido com PromiseValue = 1:
async function delay1Second() {
  return 1;
}
const result = await Promise.resolve(1); // let's call it "newPromise"
const result = await newPromise; 
// newPromise.[[PromiseStatus]]: 'resolved'
// newPromise.[[PromiseValue]]: 1
const result = 1; 
  • Caso 2 : delayPromiseé rejeitado:
// jump to catch block inside `delay1Second` if there is any
// let's say a value -1 is returned in the end
const result = await Promise.resolve(-1); // call it newPromise
const result = await newPromise;
// newPromise.[[PromiseStatus]]: 'resolved'
// newPromise.[[PromiseValue]]: -1
const result = -1;

Glossário:

  • Liquidar: Promise.[[PromiseStatus]]mudanças de pendingpara resolvedourejected
Ragtime
fonte