Promessas de JavaScript - rejeitar x lançar

385

Eu li vários artigos sobre esse assunto, mas ainda não está claro para mim se há uma diferença entre Promise.rejectvs. lançar um erro. Por exemplo,

Usando Promise.reject

return asyncIsPermitted()
    .then(function(result) {
        if (result === true) {
            return true;
        }
        else {
            return Promise.reject(new PermissionDenied());
        }
    });

Usando throw

return asyncIsPermitted()
    .then(function(result) {
        if (result === true) {
            return true;
        }
        else {
            throw new PermissionDenied();
        }
    });

Minha preferência é usar throwsimplesmente porque é mais curto, mas queria saber se existe alguma vantagem de uma sobre a outra.

Naresh
fonte
9
Ambos os métodos produzem exatamente a mesma resposta. O .then()manipulador captura a exceção lançada e a transforma em uma promessa rejeitada automaticamente. Como li que as exceções lançadas não são particularmente rápidas de executar, acho que devolver a promessa rejeitada pode ser um pouco mais rápida de executar, mas você teria que criar um teste em vários navegadores modernos, se isso fosse importante. Eu pessoalmente uso throwporque gosto da legibilidade.
jfriend00
@webduvet não com Promises - eles são projetados para trabalhar com throw.
joews
15
Uma desvantagem throwé que isso não resultaria em uma promessa rejeitada se fosse lançada de dentro de um retorno de chamada assíncrono, como um setTimeout. jsfiddle.net/m07van33 @Blondie, sua resposta estava correta.
Kevin B
@joews isso não significa que é bom;) #
webduvet 30/10
11
Ah verdade. Portanto, um esclarecimento para o meu comentário seria "se ele foi lançado de dentro de um retorno de chamada assíncrono que não foi prometido " . Eu sabia que havia uma exceção a isso, eu simplesmente não conseguia lembrar o que era. Eu também prefiro usar throw simplesmente porque acho que é mais legível e me permite omiti- rejectlo da minha lista de parâmetros.
Kevin B

Respostas:

346

Não há vantagem em usar um vs o outro, mas há um caso específico em throwque não funciona. No entanto, esses casos podem ser corrigidos.

Sempre que você estiver dentro de um retorno de chamada promissor, poderá usá-lo throw. No entanto, se você estiver em outro retorno de chamada assíncrono, deverá usar reject.

Por exemplo, isso não acionará a captura:

new Promise(function() {
  setTimeout(function() {
    throw 'or nah';
    // return Promise.reject('or nah'); also won't work
  }, 1000);
}).catch(function(e) {
  console.log(e); // doesn't happen
});

Em vez disso, você fica com uma promessa não resolvida e uma exceção não capturada. Esse é um caso em que você deseja usar reject. No entanto, você pode corrigir isso de duas maneiras.

  1. usando a função de rejeição da promessa original dentro do tempo limite:

new Promise(function(resolve, reject) {
  setTimeout(function() {
    reject('or nah');
  }, 1000);
}).catch(function(e) {
  console.log(e); // works!
});

  1. promisificando o tempo limite:

function timeout(duration) { // Thanks joews
  return new Promise(function(resolve) {
    setTimeout(resolve, duration);
  });
}

timeout(1000).then(function() {
  throw 'worky!';
  // return Promise.reject('worky'); also works
}).catch(function(e) {
  console.log(e); // 'worky!'
});

Kevin B
fonte
54
Vale ressaltar que os lugares em um retorno de chamada assíncrona não prometido que você não pode usar throw error, também não podem usar return Promise.reject(err)qual é o que o OP estava nos pedindo para comparar. É basicamente por isso que você não deve colocar retornos de chamada assíncronos dentro das promessas. Promise tudo que é assíncrono e você não tem essas restrições.
jfriend00
9
"No entanto, se você estiver em outro tipo de retorno de chamada" realmente deve ser "No entanto, se você estiver em outro tipo de retorno de chamada assíncrono ". Os retornos de chamada podem ser síncronos (por exemplo, com Array#forEach) e com aqueles, jogando dentro deles funcionaria.
Félix Saparelli
2
@KevinB lendo estas linhas "há um caso específico em que o lançamento não funcionará". e "Sempre que você estiver dentro de um retorno de chamada promissor, poderá usar o throw. No entanto, se estiver em outro retorno de chamada assíncrono, use rejeitar". Tenho a sensação de que os trechos de exemplo mostrarão casos em throwque não funcionarão e Promise.rejecté uma escolha melhor. No entanto, os trechos não são afetados por nenhuma dessas duas opções e fornecem o mesmo resultado, independentemente do que você escolher. Estou esquecendo de algo?
Anshul
2
sim. se você usar throw em um setTimeout, a captura não será chamada. você deve usar o rejectque foi passado para o new Promise(fn)retorno de chamada.
Kevin B
2
@ KevinB obrigado por ficar junto. O exemplo dado pelo OP menciona que ele especificamente queria comparar return Promise.reject()e throw. Ele não menciona o rejectretorno de chamada fornecido na new Promise(function(resolve, reject))construção. Portanto, enquanto seus dois trechos demonstram corretamente quando você deve usar o retorno de chamada de resolução, a pergunta do OP não era essa.
Anshul
202

Outro fato importante é que reject() NÃO encerra o fluxo de controle como uma returninstrução. Em contraste throw, termina o fluxo de controle.

Exemplo:

new Promise((resolve, reject) => {
  throw "err";
  console.log("NEVER REACHED");
})
.then(() => console.log("RESOLVED"))
.catch(() => console.log("REJECTED"));

vs

new Promise((resolve, reject) => {
  reject(); // resolve() behaves similarly
  console.log("ALWAYS REACHED"); // "REJECTED" will print AFTER this
})
.then(() => console.log("RESOLVED"))
.catch(() => console.log("REJECTED"));

lukyer
fonte
51
Bem, o ponto está correto, mas a comparação é complicada. Como normalmente você deve retornar sua promessa rejeitada escrevendo return reject(), para que a próxima linha não seja executada.
AZ.
7
Por que você gostaria de devolvê-lo?
Lukyer
31
Nesse caso, return reject()é simplesmente um atalho para, por exemplo, o reject(); returnque você deseja é encerrar o fluxo. O valor de retorno do executor (a função passada para new Promise) não é usado, portanto isso é seguro.
Félix Saparelli
47

Sim, a maior diferença é que rejeitar é uma função de retorno de chamada que é executada após a promessa ser rejeitada, enquanto que o throw não pode ser usado de forma assíncrona. Se você optar por usar rejeitar, seu código continuará sendo executado normalmente de maneira assíncrona, enquanto o throw priorizará a conclusão da função de resolução (essa função será executada imediatamente).

Um exemplo que vi que ajudou a esclarecer o problema para mim foi que você pode definir uma função Timeout com rejeitar, por exemplo:

new Promise(_, reject) {
 setTimeout(reject, 3000);
});

Não foi possível escrever acima com throw.

No seu pequeno exemplo, a diferença é indistinguível, mas ao lidar com um conceito assíncrono mais complicado, a diferença entre os dois pode ser drástica.

Blondie
fonte
11
Isso soa como um conceito-chave, mas não o entendo como está escrito. Ainda é muito novo para Promessas, eu acho.
David Spector
43

TLDR: uma função é difícil de usar quando, às vezes, retorna uma promessa e às vezes gera uma exceção. Ao escrever uma função assíncrona, prefira sinalizar falha retornando uma promessa rejeitada

Seu exemplo em particular ofusca algumas distinções importantes entre eles:

Como você está manipulando erros dentro de uma cadeia de promessas, as exceções lançadas são convertidas automaticamente em promessas rejeitadas. Isso pode explicar por que eles parecem ser intercambiáveis ​​- eles não são.

Considere a situação abaixo:

checkCredentials = () => {
    let idToken = localStorage.getItem('some token');
    if ( idToken ) {
      return fetch(`https://someValidateEndpoint`, {
        headers: {
          Authorization: `Bearer ${idToken}`
        }
      })
    } else {
      throw new Error('No Token Found In Local Storage')
    }
  }

Isso seria um antipadrão, pois você precisaria oferecer suporte a casos de erro assíncrono e sincronizado. Pode parecer algo como:

try {
  function onFulfilled() { ... do the rest of your logic }
  function onRejected() { // handle async failure - like network timeout }
  checkCredentials(x).then(onFulfilled, onRejected);
} catch (e) {
  // Error('No Token Found In Local Storage')
  // handle synchronous failure
} 

Não é bom e é exatamente aqui que Promise.reject(disponível no escopo global) vem o resgate e se diferencia efetivamente throw. O refator passa a ser:

checkCredentials = () => {
  let idToken = localStorage.getItem('some_token');
  if (!idToken) {
    return Promise.reject('No Token Found In Local Storage')
  }
  return fetch(`https://someValidateEndpoint`, {
    headers: {
      Authorization: `Bearer ${idToken}`
    }
  })
}

Agora, você pode usar apenas um catch()para falhas de rede e a verificação de erro síncrono por falta de tokens:

checkCredentials()
      .catch((error) => if ( error == 'No Token' ) {
      // do no token modal
      } else if ( error === 400 ) {
      // do not authorized modal. etc.
      }
maxwell
fonte
11
O exemplo de Op sempre retorna uma promessa, no entanto. A questão está se referindo se você deve usar Promise.rejectou throwquando deseja retornar uma promessa rejeitada (uma promessa que passará para a próxima .catch()).
Marcos Pereira
@ Maxwell - eu gosto de você exemplo. Ao mesmo tempo, se na busca você adicionar uma captura e nela você lançar a exceção, será seguro usar try ... catch ... Não existe um mundo perfeito no fluxo de exceções, mas acho que usar uma um padrão único faz sentido e a combinação dos padrões não é segura (alinhada com o padrão versus a analogia antipadrão).
user3053247
11
Resposta excelente, mas acho aqui uma falha - esse padrão assume que todos os erros são tratados retornando um Promise.reject - o que acontece com todos os erros inesperados que simplesmente podem ser gerados por checkCredentials ()?
chenop 4/09/18
11
Sim, você está @chenop direita - para pegar esses erros inesperados que você precisa para embrulhar em try / catch ainda
Maxwell
Não entendo o caso de @ maxwell. Você não poderia apenas estruturá-lo da mesma forma checkCredentials(x).then(onFulfilled).catch(e) {}e catchlidar com o caso de rejeição e o caso de erro lançado?
Ben Wheeler
5

Um exemplo para experimentar. Apenas mude isVersionThrow para false para usar rejeitar em vez de lançar.

const isVersionThrow = true

class TestClass {
  async testFunction () {
    if (isVersionThrow) {
      console.log('Throw version')
      throw new Error('Fail!')
    } else {
      console.log('Reject version')
      return new Promise((resolve, reject) => {
        reject(new Error('Fail!'))
      })
    }
  }
}

const test = async () => {
  const test = new TestClass()
  try {
    var response = await test.testFunction()
    return response 
  } catch (error) {
    console.log('ERROR RETURNED')
    throw error 
  }  
}

test()
.then(result => {
  console.log('result: ' + result)
})
.catch(error => {
  console.log('error: ' + error)
})

Chris Livdahl
fonte