As cadeias de promessas são ótimas no tratamento de erros. Quando uma promessa é rejeitada, o controle passa para o manipulador de rejeição mais próximo. Isso é muito conveniente na prática.
Por exemplo, no código abaixo o URL a ser fetch
está errado (não existe tal site) e .catch
trata o erro:
fetch('https://no-such-server.blabla') // rejeita .then(resposta => resposta.json()) .catch(err => alert(err)) // TypeError: falha na busca (o texto pode variar)
Como você pode ver, o .catch
não precisa ser imediato. Pode aparecer depois de um ou talvez vários .then
.
Ou talvez esteja tudo bem com o site, mas a resposta não é JSON válida. A maneira mais fácil de detectar todos os erros é anexar .catch
ao final da cadeia:
buscar('https://javascript.info/article/promise-chaining/user.json') .then(resposta => resposta.json()) .then(usuário => fetch(`https://api.github.com/users/${user.name}`)) .then(resposta => resposta.json()) .then(githubUser => new Promise((resolver, rejeitar) => { deixe img = document.createElement('img'); img.src=githubUser.avatar_url; img.className = "exemplo-avatar-promessa"; documento.body.append(img); setTimeout(() => { img.remove(); resolver(githubUser); }, 3.000); })) .catch(erro => alerta(erro.mensagem));
Normalmente, esse .catch
não é acionado. Mas se alguma das promessas acima for rejeitada (um problema de rede ou json inválido ou qualquer outra coisa), então ela será detectada.
O código de um executor de promessa e de manipuladores de promessa tem um “ try..catch
invisível” em torno dele. Se ocorrer uma exceção, ela será detectada e tratada como uma rejeição.
Por exemplo, este código:
nova promessa((resolver, rejeitar) => { throw new Error("Opa!"); }).catch(alerta); // Erro: Opa!
…Funciona exatamente da mesma forma que isto:
nova promessa((resolver, rejeitar) => { rejeitar(novo Erro("Opa!")); }).catch(alerta); // Erro: Opa!
O “ try..catch
invisível” ao redor do executor captura automaticamente o erro e o transforma em promessa rejeitada.
Isso acontece não apenas na função executora, mas também em seus manipuladores. Se throw
dentro de um manipulador .then
, isso significa uma promessa rejeitada, então o controle salta para o manipulador de erros mais próximo.
Aqui está um exemplo:
nova promessa((resolver, rejeitar) => { resolver("ok"); }).então((resultado) => { throw new Error("Opa!"); // rejeita a promessa }).catch(alerta); // Erro: Opa!
Isso acontece com todos os erros, não apenas com aqueles causados pela instrução throw
. Por exemplo, um erro de programação:
nova promessa((resolver, rejeitar) => { resolver("ok"); }).então((resultado) => { blabla(); //não existe tal função }).catch(alerta); // ReferenceError: blabla não está definido
O .catch
final não apenas captura rejeições explícitas, mas também erros acidentais nos manipuladores acima.
Como já notamos, .catch
no final da cadeia é semelhante a try..catch
. Podemos ter quantos manipuladores .then
quisermos e então usar um único .catch
no final para tratar erros em todos eles.
Em um try..catch
normal, podemos analisar o erro e talvez relançá-lo se não puder ser tratado. A mesma coisa é possível para promessas.
Se throw
dentro de .catch
, o controle irá para o próximo manipulador de erros mais próximo. E se tratarmos o erro e terminarmos normalmente, ele continuará para o próximo manipulador .then
bem-sucedido mais próximo.
No exemplo abaixo, o .catch
trata o erro com sucesso:
// a execução: catch -> then nova promessa((resolver, rejeitar) => { throw new Error("Opa!"); }).catch(função(erro) { alert("O erro foi tratado, continue normalmente"); }).then(() => alert("Próximas execuções do manipulador bem-sucedidas"));
Aqui o bloco .catch
termina normalmente. Portanto, o próximo manipulador .then
bem-sucedido é chamado.
No exemplo abaixo vemos a outra situação com .catch
. O manipulador (*)
captura o erro e simplesmente não consegue lidar com ele (por exemplo, ele só sabe como lidar com URIError
), então ele o lança novamente:
//a execução: catch -> catch nova promessa((resolver, rejeitar) => { throw new Error("Opa!"); }).catch(função(erro) { // (*) if (erro instância de URIError) { // lidar com isso } outro { alert("Não é possível lidar com esse erro"); erro de lançamento; // lançar este ou outro erro salta para a próxima captura } }).então(função() { /* não roda aqui */ }).catch(erro => { // (**) alert(`Ocorreu um erro desconhecido: ${error}`); // não retorna nada => a execução segue normalmente });
A execução salta do primeiro .catch
(*)
para o próximo (**)
na cadeia.
O que acontece quando um erro não é tratado? Por exemplo, esquecemos de acrescentar .catch
ao final da cadeia, como aqui:
nova promessa(function() { noSuchFunction(); // Erro aqui (não existe tal função) }) .então(() => { // manipuladores de promessas bem-sucedidos, um ou mais }); // sem .catch no final!
Em caso de erro, a promessa é rejeitada e a execução deve saltar para o manipulador de rejeição mais próximo. Mas não há nenhum. Então o erro fica “travado”. Não há código para lidar com isso.
Na prática, assim como acontece com erros regulares não tratados no código, isso significa que algo deu terrivelmente errado.
O que acontece quando ocorre um erro normal e não é detectado por try..catch
? O script morre com uma mensagem no console. Algo semelhante acontece com rejeições de promessas não tratadas.
O mecanismo JavaScript rastreia essas rejeições e gera um erro global nesse caso. Você pode vê-lo no console se executar o exemplo acima.
No navegador podemos detectar esses erros usando o evento unhandledrejection
:
window.addEventListener('rejeição não tratada', function(evento) { // o objeto de evento possui duas propriedades especiais: alerta(evento.promessa); // [object Promise] - a promessa que gerou o erro alerta(evento.motivo); // Erro: Opa! - o objeto de erro não tratado }); nova promessa(function() { throw new Error("Opa!"); }); // sem captura para tratar o erro
O evento faz parte do padrão HTML.
Se ocorrer um erro e não houver .catch
, o manipulador unhandledrejection
é acionado e obtém o objeto event
com as informações sobre o erro, para que possamos fazer algo.
Normalmente tais erros são irrecuperáveis, então nossa melhor saída é informar o usuário sobre o problema e provavelmente reportar o incidente ao servidor.
Em ambientes que não são de navegador, como Node.js, existem outras maneiras de rastrear erros não tratados.
.catch
lida com erros em promessas de todos os tipos: seja uma chamada reject()
ou um erro gerado em um manipulador.
.then
também captura erros da mesma maneira, se receber o segundo argumento (que é o manipulador de erros).
Devemos colocar .catch
exatamente nos locais onde queremos tratar os erros e saber como lidar com eles. O manipulador deve analisar erros (ajuda de classes de erro personalizadas) e relançar erros desconhecidos (talvez sejam erros de programação).
Não há problema em não usar .catch
, se não houver como se recuperar de um erro.
Em qualquer caso, deveríamos ter o manipulador de eventos unhandledrejection
(para navegadores e análogos para outros ambientes) para rastrear erros não tratados e informar o usuário (e provavelmente nosso servidor) sobre eles, para que nosso aplicativo nunca “simplesmente morra”.
O que você acha? O .catch
será acionado? Explique sua resposta.
nova Promessa(função(resolver, rejeitar) { setTimeout(() => { throw new Error("Opa!"); }, 1000); }).catch(alerta);
A resposta é: não, não vai :
nova Promessa(função(resolver, rejeitar) { setTimeout(() => { throw new Error("Opa!"); }, 1000); }).catch(alerta);
Como dito no capítulo, há um “ try..catch
implícito” em torno do código da função. Portanto, todos os erros síncronos são tratados.
Mas aqui o erro não é gerado enquanto o executor está em execução, mas mais tarde. Portanto, a promessa não aguenta.