Preciso retornar após a resolução / rejeição antecipada?

262

Suponha que eu tenha o seguinte código.

function divide(numerator, denominator) {
 return new Promise((resolve, reject) => {

  if(denominator === 0){
   reject("Cannot divide by 0");
   return; //superfluous?
  }

  resolve(numerator / denominator);

 });
}

Se meu objetivo é rejectsair mais cedo, devo adquirir o hábito de também returnimediatamente depois?

sam
fonte
5
Sim, devido à execução até a conclusão

Respostas:

371

O returnobjetivo é encerrar a execução da função após a rejeição e impedir a execução do código após ela.

function divide(numerator, denominator) {
  return new Promise((resolve, reject) => {

    if (denominator === 0) {
      reject("Cannot divide by 0");
      return; // The function execution ends here 
    }

    resolve(numerator / denominator);
  });
}

Nesse caso, impede a resolve(numerator / denominator);execução, o que não é estritamente necessário. No entanto, ainda é preferível encerrar a execução para evitar uma possível interceptação no futuro. Além disso, é uma boa prática evitar a execução desnecessária de código.

fundo

Uma promessa pode estar em um dos três estados:

  1. pendente - estado inicial. De pendente, podemos passar para um dos outros estados
  2. cumprida - operação bem sucedida
  3. rejeitado - operação com falha

Quando uma promessa é cumprida ou rejeitada, ela permanece neste estado indefinidamente (liquidada). Portanto, rejeitar uma promessa cumprida ou cumprir uma promessa rejeitada não terá efeito.

Este trecho de exemplo mostra que, embora a promessa tenha sido cumprida após ser rejeitada, ela permaneceu rejeitada.

function divide(numerator, denominator) {
  return new Promise((resolve, reject) => {
    if (denominator === 0) {
      reject("Cannot divide by 0");
    }

    resolve(numerator / denominator);
  });
}

divide(5,0)
  .then((result) => console.log('result: ', result))
  .catch((error) => console.log('error: ', error));

Então, por que precisamos retornar?

Embora não possamos alterar um estado de promessa estabelecida, rejeitar ou resolver não interromperá a execução do restante da função. A função pode conter código que criará resultados confusos. Por exemplo:

function divide(numerator, denominator) {
  return new Promise((resolve, reject) => {
    if (denominator === 0) {
      reject("Cannot divide by 0");
    }
    
    console.log('operation succeeded');

    resolve(numerator / denominator);
  });
}

divide(5, 0)
  .then((result) => console.log('result: ', result))
  .catch((error) => console.log('error: ', error));

Mesmo que a função não contenha esse código no momento, isso cria uma possível armadilha futura. Um refatorador futuro pode ignorar o fato de que o código ainda é executado após a promessa ser rejeitada e será difícil depurar.

Parando a execução após resolver / rejeitar:

Esse é o material padrão do fluxo de controle JS.

  • Retornar após o resolve/ reject:

  • Retorno com resolve/ reject- como o valor de retorno do retorno de chamada é ignorado, podemos salvar uma linha retornando a instrução rejeitar / resolver:

  • Usando um bloco if / else:

Prefiro usar uma das returnopções, pois o código é mais plano.

Ori Drori
fonte
28
Vale a pena notar que o código não se comportará de maneira diferente se returnestiver lá ou não, porque uma vez que um estado de promessa foi definido, ele não poderá ser alterado, portanto, a chamada resolve()após a chamada reject()não fará nada, exceto usar alguns ciclos extras da CPU. Eu próprio usaria o returnjust do ponto de vista de limpeza e eficiência do código, mas não é necessário neste exemplo específico.
jfriend00
1
Tente usar em Promise.try(() => { })vez da nova promessa e evite usar chamadas de resolver / rejeitar. Em vez disso, você pode escrever que return denominator === 0 ? throw 'Cannot divide by zero' : numerator / denominator; eu uso Promise.trycomo meio de iniciar uma promessa e eliminar promessas envolvidas em blocos try / catch que são problemáticos.
kingdango 27/09/16
2
É bom saber e eu gosto do padrão. No entanto, neste momento Promise.try é um estágio 0 sugestão, para que você só pode usá-lo com um calço ou usando uma biblioteca de promessa, como bluebird ou Q.
Ori Drori
6
@ jfriend00 Obviamente, neste exemplo simples, o código não se comportará de maneira diferente. Mas e se você tivesse um código depois do rejectque faz algo caro, como conectar-se a bancos de dados ou pontos finais de API? Tudo isso seria desnecessário e custaria dinheiro e recursos, especialmente por exemplo, se você estiver se conectando a algo como um banco de dados da AWS ou um ponto de extremidade do API Gateway. Nesse caso, você definitivamente usaria um retorno para evitar que códigos desnecessários fossem executados.
Jake Wilson
3
@JakeWilson - Claro, isso é apenas o fluxo normal de código em Javascript e não tem nada a ver com promessas. Se você terminar de processar a função e não desejar mais executar nenhum código no caminho atual, insira a return.
jfriend00
37

Um idioma comum, que pode ou não ser sua xícara de chá, é combinar o returncom o reject, para rejeitar simultaneamente a promessa e sair da função, para que o restante da função, incluindo o, resolvenão seja executado. Se você gosta desse estilo, ele pode tornar seu código um pouco mais compacto.

function divide(numerator, denominator) {
  return new Promise((resolve, reject) => {
    if (denominator === 0) return reject("Cannot divide by 0");
                           ^^^^^^^^^^^^^^
    resolve(numerator / denominator);
  });
}

Isso funciona bem porque o construtor Promise não faz nada com nenhum valor de retorno e, em qualquer caso, resolvee rejectnão retorna nada.

O mesmo idioma pode ser usado com o estilo de retorno de chamada mostrado em outra resposta:

function divide(nom, denom, cb){
  if(denom === 0) return cb(Error("Cannot divide by zero"));
                  ^^^^^^^^^
  cb(null, nom / denom);
} 

Novamente, isso funciona bem porque a pessoa que divideestá chamando não espera que retorne nada e não faz nada com o valor de retorno.


fonte
6
Eu não gosto disso. Isso dá a noção de que você está retornando algo que de fato não está. Você está chamando a função rejeitar e, em seguida, usa return para finalizar a execução da função. Mantenha-os em linhas separadas, o que você está fazendo apenas confundirá as pessoas. A legibilidade do código é fundamental.
K - A toxicidade no SO está crescendo.
7
@KarlMorrison, de fato, você está retornando "algo", uma promessa rejeitada. Eu acho que a "noção" de que você está falando é muito pessoal. Não há nada de errado em retornar um rejectstatus
Frondor
5
@ Frondor Acho que você não entendeu o que escrevi. Claro que você e eu entendemos isso, nada acontece ao retornar uma rejeição na mesma linha. Mas e os desenvolvedores que não estão tão acostumados ao JavaScript que entram em um projeto? Esse tipo de programação diminui a legibilidade para essas pessoas. Hoje, o ecossistema JavaScript já é uma bagunça o suficiente e as pessoas que espalham esse tipo de prática só pioram. Isso é uma má prática.
K - A toxicidade no SO está crescendo.
1
@KarlMorrison Opiniões pessoais! = Más práticas. Provavelmente, ajudaria um novo desenvolvedor Javascript a entender o que está acontecendo com o retorno.
precisa saber é o seguinte
1
@TobyCaulk Se as pessoas precisam aprender com o retorno, não devem brincar com o Promises, devem aprender a programação básica.
K - A toxicidade no SO está crescendo.
10

Tecnicamente , não é necessário aqui 1 - porque uma Promessa pode ser resolvida ou rejeitada, exclusivamente e apenas uma vez. O primeiro resultado da promessa vence e todos os resultados subsequentes são ignorados. Isso é diferente dos retornos de chamada no estilo do nó.

Dito isto, é uma boa prática limpa garantir que exatamente um seja chamado, quando prático, e, de fato, neste caso, uma vez que não há mais processamento assíncrono / adiado. A decisão de "retornar cedo" não é diferente de terminar qualquer função quando seu trabalho estiver concluído - em vez de continuar o processamento não relacionado ou desnecessário.

Retornar no momento apropriado (ou usar condicionais para evitar a execução do "outro caso") reduz a chance de executar código acidentalmente em um estado inválido ou de executar efeitos colaterais indesejados; e, como tal, torna o código menos propenso a 'quebrar inesperadamente'.


1 Essa resposta tecnicamente também depende do fato de que , neste caso, o código após o "retorno", caso seja omitido, não resultará em um efeito colateral. Felizmente , o JavaScript será dividido por zero e retornará + Infinity / -Infinity ou NaN.

user2864740
fonte
Nota de rodapé agradável !!
HankCa
9

Se você não "retornar" após uma resolução / rejeição, coisas ruins (como um redirecionamento de página) poderão acontecer depois que você tiver a intenção de parar. Fonte: Corri para isso.

Benjamin H
fonte
6
+1 para o exemplo. Eu tive um problema em que meu programa fazia mais de 100 consultas inválidas ao banco de dados e não conseguia descobrir o motivo. Acontece que eu não "retornei" após uma rejeição. É um pequeno erro, mas eu aprendi minha lição.
AdamInTheOculus 23/03
8

A resposta de Ori já explica que não é necessário, returnmas é uma boa prática. Observe que o construtor de promessas é seguro, portanto ignorará as exceções lançadas passadas posteriormente no caminho. Essencialmente, você tem efeitos colaterais que não podem ser observados facilmente.

Observe que o returninício precoce também é muito comum em retornos de chamada:

function divide(nom, denom, cb){
     if(denom === 0){
         cb(Error("Cannot divide by zero");
         return; // unlike with promises, missing the return here is a mistake
     }
     cb(null, nom / denom); // this will divide by zero. Since it's a callback.
} 

Portanto, embora seja uma boa prática em promessas, é necessário com retornos de chamada. Algumas notas sobre o seu código:

  • Seu caso de uso é hipotético, não use promessas com ações síncronas.
  • O construtor da promessa ignora os valores retornados. Algumas bibliotecas avisam se você retornar um valor não indefinido para avisá-lo contra o erro de retornar para lá. A maioria não é tão inteligente.
  • O construtor da promessa é seguro, converterá exceções em rejeições, mas como outros já apontaram - uma promessa é resolvida uma vez.
Benjamin Gruenbaum
fonte
4

Em muitos casos, é possível validar parâmetros separadamente e retornar imediatamente uma promessa rejeitada com Promise.reject (reason) .

function divide2(numerator, denominator) {
  if (denominator === 0) {
    return Promise.reject("Cannot divide by 0");
  }
  
  return new Promise((resolve, reject) => {
    resolve(numerator / denominator);
  });
}


divide2(4, 0).then((result) => console.log(result), (error) => console.log(error));

Dorad
fonte