Não importa quão bons sejamos em programação, às vezes nossos scripts apresentam erros. Eles podem ocorrer devido a erros nossos, a uma entrada inesperada do usuário, a uma resposta errada do servidor e por milhares de outros motivos.
Normalmente, um script “morre” (para imediatamente) em caso de erro, imprimindo-o no console.
Mas há uma construção de sintaxe try...catch
que nos permite “capturar” erros para que o script possa, em vez de morrer, fazer algo mais razoável.
A construção try...catch
tem dois blocos principais: try
e então catch
:
tentar { //código... } pegar (errar) { //tratamento de erros }
Funciona assim:
Primeiro, o código em try {...}
é executado.
Se não houve erros, então catch (err)
é ignorado: a execução chega ao final do try
e continua, ignorando catch
.
Se ocorrer um erro, a execução try
será interrompida e o controle fluirá para o início do catch (err)
. A variável err
(podemos usar qualquer nome para ela) conterá um objeto de erro com detalhes sobre o que aconteceu.
Portanto, um erro dentro do bloco try {...}
não mata o script – temos a chance de lidar com isso em catch
.
Vejamos alguns exemplos.
Um exemplo sem erros: mostra alert
(1)
e (2)
:
tentar { alert('Início da tentativa'); // (1) <-- // ...nenhum erro aqui alert('Fim da tentativa'); // (2) <-- } pegar (errar) { alert('Catch é ignorado porque não há erros'); // (3) }
Um exemplo com erro: mostra (1)
e (3)
:
tentar { alert('Início da tentativa'); // (1) <-- lalala; // erro, variável não definida! alert('Fim da tentativa (nunca alcançado)'); // (2) } pegar (errar) { alert(`Ocorreu um erro!`); // (3) <-- }
try...catch
só funciona para erros de tempo de execução
Para try...catch
funcione, o código deve ser executável. Em outras palavras, deve ser JavaScript válido.
Não funcionará se o código estiver sintaticamente errado, por exemplo, se tiver chaves incomparáveis:
tentar { {{{{{{{{{{{{ } pegar (errar) { alert("O mecanismo não consegue entender este código, é inválido"); }
O mecanismo JavaScript primeiro lê o código e depois o executa. Os erros que ocorrem na fase de leitura são chamados de erros de “tempo de análise” e são irrecuperáveis (de dentro desse código). Isso ocorre porque o mecanismo não consegue entender o código.
Portanto, try...catch
só pode lidar com erros que ocorrem em código válido. Tais erros são chamados de “erros de tempo de execução” ou, às vezes, “exceções”.
try...catch
funciona de forma síncrona
Se uma exceção acontecer no código “agendado”, como em setTimeout
, então try...catch
não irá capturá-la:
tentar { setTimeout(função(){ nãoSuchVariable; // o script morrerá aqui }, 1000); } pegar (errar) { alerta("não funciona"); }
Isso ocorre porque a função em si é executada posteriormente, quando o mecanismo já saiu da construção try...catch
.
Para capturar uma exceção dentro de uma função agendada, try...catch
deve estar dentro dessa função:
setTimeout(função(){ tentar { nãoSuchVariable; // try...catch trata o erro! } pegar { alert("erro detectado aqui!"); } }, 1000);
Quando ocorre um erro, o JavaScript gera um objeto contendo detalhes sobre ele. O objeto é então passado como argumento para catch
:
tentar { // ... } catch (err) { // <-- o "objeto de erro", poderia usar outra palavra em vez de err // ... }
Para todos os erros internos, o objeto de erro possui duas propriedades principais:
name
Nome do erro. Por exemplo, para uma variável indefinida que é "ReferenceError"
.
message
Mensagem de texto sobre detalhes do erro.
Existem outras propriedades não padronizadas disponíveis na maioria dos ambientes. Um dos mais utilizados e suportados é:
stack
Pilha de chamadas atual: uma string com informações sobre a sequência de chamadas aninhadas que levaram ao erro. Usado para fins de depuração.
Por exemplo:
tentar { lalala; // erro, variável não definida! } pegar (errar) { alerta(err.nome); //Erro de referência alerta(err.mensagem); // lalala não está definido alerta(err.stack); // ReferenceError: lalala não está definido em (...pilha de chamadas) // Também pode mostrar um erro como um todo // O erro é convertido em string como "nome: mensagem" alerta(erro); // ReferenceError: lalala não está definido }
Uma adição recente
Esta é uma adição recente ao idioma. Navegadores antigos podem precisar de polyfills.
Se não precisarmos de detalhes do erro, catch
poderá omiti-lo:
tentar { // ... } catch { // <-- sem (err) // ... }
Vamos explorar um caso de uso real de try...catch
.
Como já sabemos, JavaScript suporta o método JSON.parse(str) para ler valores codificados em JSON.
Geralmente é usado para decodificar dados recebidos pela rede, do servidor ou de outra fonte.
Recebemos e chamamos JSON.parse
assim:
deixe json = '{"nome":"João", "idade": 30}'; //dados do servidor deixe usuário = JSON.parse(json); //converte a representação de texto em objeto JS // agora o usuário é um objeto com propriedades da string alerta(usuário.nome); // John alerta(usuário.idade); //30
Você pode encontrar informações mais detalhadas sobre JSON no capítulo Métodos JSON, toJSON.
Se json
estiver malformado, JSON.parse
gera um erro, então o script “morre”.
Deveríamos estar satisfeitos com isso? Claro que não!
Dessa forma, se algo estiver errado com os dados, o visitante nunca saberá disso (a menos que abra o console do desenvolvedor). E as pessoas realmente não gostam quando algo “simplesmente morre” sem nenhuma mensagem de erro.
Vamos usar try...catch
para lidar com o erro:
deixe json = "{json ruim}"; tentar { deixe usuário = JSON.parse(json); // <-- quando ocorre um erro... alerta(usuário.nome); //não funciona } pegar (errar) { // ...a execução salta aqui alert( "Pedimos desculpas, os dados apresentam erros, tentaremos solicitá-los mais uma vez." ); alerta(err.nome); alerta(err.mensagem); }
Aqui usamos o bloco catch
apenas para mostrar a mensagem, mas podemos fazer muito mais: enviar uma nova solicitação de rede, sugerir uma alternativa ao visitante, enviar informações sobre o erro para um recurso de registro,…. Tudo muito melhor do que simplesmente morrer.
E se json
estiver sintaticamente correto, mas não tiver uma propriedade name
obrigatória?
Assim:
deixe json = '{ "idade": 30 }'; //dados incompletos tentar { deixe usuário = JSON.parse(json); // <-- sem erros alerta(usuário.nome); // sem nome! } pegar (errar) { alert("não executa"); }
Aqui JSON.parse
roda normalmente, mas a ausência do name
é na verdade um erro para nós.
Para unificar o tratamento de erros, usaremos o operador throw
.
O operador throw
gera um erro.
A sintaxe é:
lançar <objeto de erro>
Tecnicamente, podemos usar qualquer coisa como objeto de erro. Isso pode até ser primitivo, como um número ou uma string, mas é melhor usar objetos, de preferência com propriedades name
e message
(para permanecer compatível com erros internos).
JavaScript possui muitos construtores integrados para erros padrão: Error
, SyntaxError
, ReferenceError
, TypeError
e outros. Também podemos usá-los para criar objetos de erro.
A sintaxe deles é:
deixe erro = novo Erro (mensagem); // ou deixe erro = new SyntaxError(mensagem); deixe erro = novo ReferenceError(mensagem); // ...
Para erros internos (não para quaisquer objetos, apenas para erros), a propriedade name
é exatamente o nome do construtor. E message
é tirada do argumento.
Por exemplo:
deixe erro = new Error("Coisas acontecem o_O"); alerta(erro.nome); // Erro alerta(erro.mensagem); //As coisas acontecem o_O
Vamos ver que tipo de erro JSON.parse
gera:
tentar { JSON.parse("{json ruim o_O }"); } pegar (errar) { alerta(err.nome); //Erro de sintaxe alerta(err.mensagem); // Token b inesperado em JSON na posição 2 }
Como podemos ver, isso é um SyntaxError
.
E no nosso caso, a ausência do name
é um erro, pois os usuários devem ter um name
.
Então vamos lançar:
deixe json = '{ "idade": 30 }'; //dados incompletos tentar { deixe usuário = JSON.parse(json); // <-- sem erros if (!usuário.nome) { throw new SyntaxError("Dados incompletos: sem nome"); // (*) } alerta(usuário.nome); } pegar (errar) { alert("Erro JSON: " + err.message ); // Erro JSON: dados incompletos: sem nome }
Na linha (*)
, o operador throw
gera um SyntaxError
com a message
fornecida, da mesma forma que o próprio JavaScript o geraria. A execução de try
para imediatamente e o fluxo de controle salta para catch
.
Agora catch
se tornou um local único para todo tratamento de erros: tanto para JSON.parse
quanto para outros casos.
No exemplo acima usamos try...catch
para lidar com dados incorretos. Mas é possível que ocorra outro erro inesperado no bloco try {...}
? Como um erro de programação (a variável não está definida) ou outra coisa, não apenas essa coisa de “dados incorretos”.
Por exemplo:
deixe json = '{ "idade": 30 }'; //dados incompletos tentar { usuário = JSON.parse(json); // <-- esqueci de colocar "let" antes do usuário // ... } pegar (errar) { alert("Erro JSON: " + err); // Erro JSON: ReferenceError: o usuário não está definido // (na verdade, nenhum erro JSON) }
Claro, tudo é possível! Os programadores cometem erros. Mesmo em utilitários de código aberto usados por milhões de pessoas durante décadas – de repente pode ser descoberto um bug que leva a hacks terríveis.
No nosso caso, try...catch
é colocado para capturar erros de “dados incorretos”. Mas, por sua natureza, catch
obtém todos os erros de try
. Aqui ocorre um erro inesperado, mas ainda mostra a mesma mensagem "JSON Error"
. Isso está errado e também torna o código mais difícil de depurar.
Para evitar tais problemas, podemos empregar a técnica de “relançamento”. A regra é simples:
O Catch deve processar apenas os erros que conhece e “relançar” todos os outros.
A técnica de “relançamento” pode ser explicada com mais detalhes como:
Catch obtém todos os erros.
No bloco catch (err) {...}
analisamos o objeto de erro err
.
Se não sabemos como lidar com isso, throw err
.
Normalmente, podemos verificar o tipo de erro usando o operador instanceof
:
tentar { usuário = { /*...*/ }; } pegar (errar) { if (errar instância de ReferenceError) { alert('Erro de Referência'); // "ReferenceError" para acessar uma variável indefinida } }
Também podemos obter o nome da classe de erro na propriedade err.name
. Todos os erros nativos possuem isso. Outra opção é ler err.constructor.name
.
No código abaixo, usamos rethrowing para que catch
lide apenas com SyntaxError
:
deixe json = '{ "idade": 30 }'; //dados incompletos tentar { deixe usuário = JSON.parse(json); if (!usuário.nome) { throw new SyntaxError("Dados incompletos: sem nome"); } blabla(); //erro inesperado alerta(usuário.nome); } pegar (errar) { if (errar instância de SyntaxError) { alert("Erro JSON: " + err.message ); } outro { lançar errar; // relançar (*) } }
O erro lançado na linha (*)
de dentro do bloco catch
“cai” de try...catch
e pode ser capturado por uma construção try...catch
externa (se existir) ou mata o script.
Portanto, o bloco catch
realmente trata apenas os erros com os quais sabe lidar e “ignora” todos os outros.
O exemplo abaixo demonstra como tais erros podem ser detectados por mais um nível de try...catch
:
função lerDados() { deixe json = '{ "idade": 30 }'; tentar { // ... blabla(); //erro! } pegar (errar) { // ... if (!(err instância de SyntaxError)) { lançar errar; // relançar (não sei como lidar com isso) } } } tentar { lerDados(); } pegar (errar) { alert("Captura externa obtida: " + err ); // peguei! }
Aqui readData
só sabe como lidar SyntaxError
, enquanto o try...catch
externo sabe como lidar com tudo.
Espere, isso não é tudo.
A construção try...catch
pode ter mais uma cláusula de código: finally
.
Se existir, é executado em todos os casos:
depois de try
, se não houve erros,
depois de catch
, se houve erros.
A sintaxe estendida é semelhante a esta:
tentar { ... tente executar o código ... } pegar (errar) { ... lidar com erros ... } finalmente { ... execute sempre ... }
Tente executar este código:
tentar { alerta('tentar'); if (confirm('Cometer um erro?')) BAD_CODE(); } pegar (errar) { alerta('captura'); } finalmente { alerta('finalmente'); }
O código possui duas formas de execução:
Se você responder “Sim” para “Cometer um erro?”, try -> catch -> finally
.
Se você disser “Não”, try -> finally
.
A cláusula finally
é frequentemente usada quando começamos a fazer algo e queremos finalizá-lo em qualquer caso de resultado.
Por exemplo, queremos medir o tempo que uma função numérica de Fibonacci fib(n)
leva. Naturalmente, podemos começar a medir antes de começar e terminar depois. Mas e se houver um erro durante a chamada da função? Em particular, a implementação de fib(n)
no código abaixo retorna um erro para números negativos ou não inteiros.
A cláusula finally
é um ótimo lugar para finalizar as medições, não importa o que aconteça.
Aqui finally
garante que o tempo será medido corretamente em ambas as situações – no caso de uma execução bem-sucedida da fib
e no caso de um erro na mesma:
deixe num = +prompt("Digite um número inteiro positivo?", 35) deixe diff, resultado; função fib(n) { se (n < 0 || Math.trunc(n) != n) { throw new Error("Não deve ser negativo e também um número inteiro."); } retornar n <= 1? n : fib(n - 1) + fib(n - 2); } vamos começar = Date.now(); tentar { resultado = fib(num); } pegar (errar) { resultado = 0; } finalmente { diff = Date.now() - início; } alert(resultado || "ocorreu um erro"); alert(`a execução demorou ${diff}ms` );
Você pode verificar executando o código digitando 35
no prompt
– ele é executado normalmente, finally
após try
. E então digite -1
– haverá um erro imediato e a execução levará 0ms
. Ambas as medições são feitas corretamente.
Em outras palavras, a função pode terminar com return
ou throw
, isso não importa. A cláusula finally
é executada em ambos os casos.
Variáveis são locais dentro de try...catch...finally
Observe que as variáveis result
e diff
no código acima são declaradas antes de try...catch
.
Caso contrário, se declarássemos o bloco let
in try
, ele só seria visível dentro dele.
finally
e return
A cláusula finally
funciona para qualquer saída de try...catch
. Isso inclui um return
explícito.
No exemplo abaixo, há um return
em try
. Nesse caso, finally
é executado logo antes do controle retornar ao código externo.
função função() { tentar { retornar 1; } pegar (errar) { /* ... */ } finalmente { alerta('finalmente'); } } alerta(função()); // primeiro funciona o alerta de finalmente, e depois este
try...finally
A construção try...finally
, sem cláusula catch
, também é útil. Nós o aplicamos quando não queremos lidar com erros aqui (deixá-los falhar), mas queremos ter certeza de que os processos que iniciamos serão finalizados.
função função() { //começa a fazer algo que precisa ser concluído (como medições) tentar { // ... } finalmente { // completa aquela coisa mesmo que todos morram } }
No código acima, sempre ocorre um erro dentro de try
, porque não há catch
. Mas finally
funciona antes que o fluxo de execução saia da função.
Específico do ambiente
As informações desta seção não fazem parte do JavaScript principal.
Vamos imaginar que tivemos um erro fatal fora de try...catch
e o script morreu. Como um erro de programação ou alguma outra coisa terrível.
Existe uma maneira de reagir a tais ocorrências? Podemos querer registrar o erro, mostrar algo ao usuário (normalmente ele não vê mensagens de erro), etc.
Não há nada na especificação, mas os ambientes geralmente fornecem isso, porque é realmente útil. Por exemplo, Node.js tem process.on("uncaughtException")
para isso. E no navegador podemos atribuir uma função à propriedade especial window.onerror, que será executada no caso de um erro não detectado.
A sintaxe:
window.onerror = function(mensagem, url, linha, coluna, erro) { // ... };
message
Mensagem de erro.
url
URL do script onde ocorreu o erro.
line
, col
Números de linha e coluna onde ocorreu o erro.
error
Objeto de erro.
Por exemplo:
<roteiro> window.onerror = function(mensagem, url, linha, coluna, erro) { alert(`${message}n Em ${line}:${col} de ${url}`); }; função lerDados() { badFunc(); // Opa, algo deu errado! } lerDados(); </script>
A função do manipulador global window.onerror
geralmente não é recuperar a execução do script – o que provavelmente é impossível em caso de erros de programação, mas enviar a mensagem de erro aos desenvolvedores.
Existem também serviços da web que fornecem registro de erros para tais casos, como https://errorception.com ou https://www.muscula.com.
Eles funcionam assim:
Nós nos registramos no serviço e obtemos deles um pedaço de JS (ou uma URL de script) para inserir nas páginas.
Esse script JS define uma função window.onerror
personalizada.
Quando ocorre um erro, ele envia uma solicitação de rede ao serviço.
Podemos fazer login na interface web do serviço e ver os erros.
A construção try...catch
permite lidar com erros de tempo de execução. Literalmente permite “tentar” executar o código e “capturar” erros que possam ocorrer nele.
A sintaxe é:
tentar { //executa este código } pegar (errar) { // se ocorreu um erro, então pule aqui // err é o objeto de erro } finalmente { // faça em qualquer caso depois de try/catch }
Pode não haver nenhuma seção catch
ou finally
, portanto construções mais curtas try...catch
e try...finally
também são válidas.
Objetos de erro possuem as seguintes propriedades:
message
– a mensagem de erro legível por humanos.
name
– a string com o nome do erro (nome do construtor do erro).
stack
(não padrão, mas bem suportada) – a pilha no momento da criação do erro.
Se um objeto de erro não for necessário, podemos omiti-lo usando catch {
em vez de catch (err) {
.
Também podemos gerar nossos próprios erros usando o operador throw
. Tecnicamente, o argumento de throw
pode ser qualquer coisa, mas geralmente é um objeto de erro herdado da classe interna Error
. Mais sobre como estender erros no próximo capítulo.
Relançar é um padrão muito importante de tratamento de erros: um bloco catch
geralmente espera e sabe como lidar com o tipo de erro específico, portanto, deve relançar erros que não conhece.
Mesmo que não tenhamos try...catch
, a maioria dos ambientes nos permite configurar um manipulador de erros “global” para capturar erros que “caem”. No navegador, é window.onerror
.
importância: 5
Compare os dois fragmentos de código.
O primeiro usa finally
para executar o código após try...catch
:
tentar { trabalho trabalho } pegar (errar) { lidar com erros } finalmente { limpar o espaço de trabalho }
O segundo fragmento coloca a limpeza logo após try...catch
:
tentar { trabalho trabalho } pegar (errar) { lidar com erros } limpar o espaço de trabalho
Definitivamente precisamos da limpeza após a obra, não importa se houve erro ou não.
Existe uma vantagem aqui em usar finally
ou ambos os fragmentos de código são iguais? Se houver tal vantagem, dê um exemplo quando for importante.
A diferença fica óbvia quando olhamos o código dentro de uma função.
O comportamento é diferente se houver um “salto” de try...catch
.
Por exemplo, quando há um return
dentro de try...catch
. A cláusula finally
funciona no caso de qualquer saída de try...catch
, mesmo por meio da instrução return
: logo após a conclusão try...catch
, mas antes que o código de chamada obtenha o controle.
função f() { tentar { alerta('iniciar'); retornar "resultado"; } pegar (errar) { /// ... } finalmente { alerta('limpeza!'); } } f(); // limpar!
…Ou quando há um throw
, como aqui:
função f() { tentar { alerta('iniciar'); lançar novo Erro("um erro"); } pegar (errar) { // ... if("não consigo lidar com o erro") { lançar errar; } } finalmente { alerta('limpeza!') } } f(); // limpar!
É finally
isso que garante a limpeza aqui. Se apenas colocássemos o código no final de f
, ele não funcionaria nessas situações.