Quando desenvolvemos algo, muitas vezes precisamos de nossas próprias classes de erros para refletir coisas específicas que podem dar errado em nossas tarefas. Para erros em operações de rede podemos precisar de HttpError
, para operações de banco de dados DbError
, para operações de pesquisa NotFoundError
e assim por diante.
Nossos erros devem suportar propriedades básicas de erro como message
, name
e, preferencialmente, stack
. Mas eles também podem ter outras propriedades próprias, por exemplo, objetos HttpError
podem ter uma propriedade statusCode
com um valor como 404
ou 403
ou 500
.
JavaScript permite usar throw
com qualquer argumento, portanto, tecnicamente, nossas classes de erro personalizadas não precisam herdar de Error
. Mas se herdarmos, será possível usar obj instanceof Error
para identificar objetos de erro. Portanto, é melhor herdar disso.
À medida que a aplicação cresce, nossos próprios erros formam naturalmente uma hierarquia. Por exemplo, HttpTimeoutError
pode herdar de HttpError
e assim por diante.
Como exemplo, vamos considerar uma função readUser(json)
que deve ler JSON com dados do usuário.
Aqui está um exemplo de como um json
válido pode parecer:
deixe json = `{ "nome": "John", "idade": 30 }`;
Internamente, usaremos JSON.parse
. Se receber json
malformado, ele lançará SyntaxError
. Mas mesmo que json
esteja sintaticamente correto, isso não significa que seja um usuário válido, certo? Pode perder os dados necessários. Por exemplo, pode não ter propriedades name
e age
essenciais para nossos usuários.
Nossa função readUser(json)
não apenas lerá JSON, mas também verificará (“validará”) os dados. Se não houver campos obrigatórios ou se o formato estiver errado, isso é um erro. E isso não é SyntaxError
, porque os dados estão sintaticamente corretos, mas outro tipo de erro. Chamaremos isso ValidationError
e criaremos uma classe para ele. Um erro desse tipo também deve conter informações sobre o campo infrator.
Nossa classe ValidationError
deve herdar da classe Error
.
A classe Error
está integrada, mas aqui está seu código aproximado para que possamos entender o que estamos estendendo:
// O "pseudocódigo" para a classe Error integrada definida pelo próprio JavaScript erro de classe { construtor(mensagem) { esta.mensagem = mensagem; this.name = "Erro"; // (nomes diferentes para diferentes classes de erro integradas) this.stack = <pilha de chamadas>; // não padrão, mas a maioria dos ambientes suporta } }
Agora vamos herdar ValidationError
dele e testá-lo em ação:
class ValidationError estende o erro { construtor(mensagem) { super(mensagem); // (1) this.name = "Erro de validação"; // (2) } } teste de função() { throw new ValidationError("Opa!"); } tentar { teste(); } pegar(errar) { alerta(err.mensagem); // Opa! alerta(err.nome); //Erro de validação alerta(err.stack); //uma lista de chamadas aninhadas com números de linha para cada uma }
Observação: na linha (1)
chamamos o construtor pai. JavaScript exige que chamemos super
no construtor filho, então isso é obrigatório. O construtor pai define a propriedade message
.
O construtor pai também define a propriedade name
como "Error"
, portanto, na linha (2)
nós a redefinimos para o valor correto.
Vamos tentar usá-lo em readUser(json)
:
class ValidationError estende o erro { construtor(mensagem) { super(mensagem); this.name = "Erro de validação"; } } // Uso função readUser(json) { deixe usuário = JSON.parse(json); if (!usuário.idade) { throw new ValidationError("Nenhum campo: idade"); } if (!usuário.nome) { throw new ValidationError("Nenhum campo: nome"); } usuário de retorno; } // Exemplo de trabalho com try..catch tentar { deixe usuário = readUser('{ "idade": 25 }'); } pegar (errar) { if (errar instância de ValidationError) { alert("Dados inválidos: " + err.message); //Dado inválido: Nenhum campo: nome } else if (err instanceof SyntaxError) { // (*) alert("Erro de sintaxe JSON: " + err.message); } outro { lançar errar; // erro desconhecido, relançá-lo (**) } }
O bloco try..catch
no código acima lida com nosso ValidationError
e o SyntaxError
integrado de JSON.parse
.
Dê uma olhada em como usamos instanceof
para verificar o tipo de erro específico na linha (*)
.
Também poderíamos olhar para err.name
, assim:
// ... // em vez de (err instanceof SyntaxError) } else if (err.name == "SyntaxError") { // (*) // ...
A versão instanceof
é muito melhor, porque no futuro iremos estender ValidationError
, criar subtipos dele, como PropertyRequiredError
. E instanceof
check continuará funcionando para novas classes herdadas. Então isso é à prova de futuro.
Também é importante que, se catch
encontrar um erro desconhecido, ele o repita na linha (**)
. O bloco catch
só sabe como lidar com erros de validação e sintaxe; outros tipos (causados por um erro de digitação no código ou outros motivos desconhecidos) devem falhar.
A classe ValidationError
é muito genérica. Muitas coisas podem dar errado. A propriedade pode estar ausente ou estar em um formato errado (como um valor de string para age
em vez de um número). Vamos fazer uma classe PropertyRequiredError
mais concreta, exatamente para propriedades ausentes. Ele conterá informações adicionais sobre a propriedade que está faltando.
class ValidationError estende o erro { construtor(mensagem) { super(mensagem); this.name = "Erro de validação"; } } class PropertyRequiredError estende ValidationError { construtor(propriedade) { super("Sem propriedade: " + propriedade); this.name = "PropertyRequiredError"; esta.propriedade = propriedade; } } // Uso função readUser(json) { deixe usuário = JSON.parse(json); if (!usuário.idade) { lançar novo PropertyRequiredError("idade"); } if (!usuário.nome) { lançar novo PropertyRequiredError("nome"); } usuário de retorno; } // Exemplo de trabalho com try..catch tentar { deixe usuário = readUser('{ "idade": 25 }'); } pegar (errar) { if (errar instância de ValidationError) { alert("Dados inválidos: " + err.message); //Dados inválidos: Nenhuma propriedade: nome alerta(err.nome); //PropertyRequiredError alerta(err.propriedade); // nome } else if (errar instância de SyntaxError) { alert("Erro de sintaxe JSON: " + err.message); } outro { lançar errar; //erro desconhecido, repita } }
A nova classe PropertyRequiredError
é fácil de usar: basta passar o nome da propriedade: new PropertyRequiredError(property)
. A message
legível por humanos é gerada pelo construtor.
Observe que this.name
no construtor PropertyRequiredError
é novamente atribuído manualmente. Isso pode se tornar um pouco tedioso – atribuir this.name = <class name>
em cada classe de erro personalizada. Podemos evitá-lo criando nossa própria classe de “erro básico” que atribui this.name = this.constructor.name
. E então herdar todos os nossos erros personalizados dele.
Vamos chamá-lo de MyError
.
Aqui está o código com MyError
e outras classes de erro personalizadas, simplificadas:
class MeuErro estende Erro { construtor(mensagem) { super(mensagem); este.nome = este.construtor.nome; } } classe ValidationError estende MyError { } class PropertyRequiredError estende ValidationError { construtor(propriedade) { super("Sem propriedade: " + propriedade); esta.propriedade = propriedade; } } //nome está correto alert(new PropertyRequiredError("campo").nome); //PropertyRequiredError
Agora os erros personalizados são muito mais curtos, especialmente ValidationError
, pois nos livramos da linha "this.name = ..."
no construtor.
O objetivo da função readUser
no código acima é “ler os dados do usuário”. Podem ocorrer diferentes tipos de erros no processo. No momento temos SyntaxError
e ValidationError
, mas no futuro a função readUser
pode crescer e provavelmente gerar outros tipos de erros.
O código que chama readUser
deve tratar esses erros. No momento, ele usa vários if
s no bloco catch
, que verificam a classe e tratam de erros conhecidos e relançam os desconhecidos.
O esquema é assim:
tentar { ... readUser() // a possível fonte de erro ... } pegar (errar) { if (errar instância de ValidationError) { // trata erros de validação } else if (errar instância de SyntaxError) { // trata erros de sintaxe } outro { lançar errar; //erro desconhecido, repita } }
No código acima podemos ver dois tipos de erros, mas pode haver mais.
Se a função readUser
gera vários tipos de erros, devemos nos perguntar: queremos realmente verificar todos os tipos de erros, um por um, todas as vezes?
Muitas vezes a resposta é “Não”: gostaríamos de estar “um nível acima de tudo isso”. Queremos apenas saber se houve um “erro de leitura de dados” – por que exatamente isso aconteceu é muitas vezes irrelevante (a mensagem de erro descreve isso). Ou, melhor ainda, gostaríamos de ter uma maneira de obter os detalhes do erro, mas apenas se for necessário.
A técnica que descrevemos aqui é chamada de “encapsulamento de exceções”.
Criaremos uma nova classe ReadError
para representar um erro genérico de “leitura de dados”.
A função readUser
capturará erros de leitura de dados que ocorrem dentro dela, como ValidationError
e SyntaxError
, e gerará um ReadError
.
O objeto ReadError
manterá a referência ao erro original em sua propriedade cause
.
Então, o código que chama readUser
terá apenas que verificar ReadError
, não todos os tipos de erros de leitura de dados. E se precisar de mais detalhes de um erro, poderá verificar sua propriedade cause
.
Aqui está o código que define ReadError
e demonstra seu uso em readUser
e try..catch
:
class ReadError estende o erro { construtor(mensagem, causa) { super(mensagem); this.cause = causa; this.name = 'ReadError'; } } class ValidationError estende o erro { /*...*/ } class PropertyRequiredError estende ValidationError { /* ... */ } function validarUsuário(usuário) { if (!usuário.idade) { lançar novo PropertyRequiredError("idade"); } if (!usuário.nome) { lançar novo PropertyRequiredError("nome"); } } função readUser(json) { deixe o usuário; tentar { usuário = JSON.parse(json); } pegar (errar) { if (errar instância de SyntaxError) { throw new ReadError("Erro de sintaxe", err); } outro { lançar errar; } } tentar { validarUsuário(usuário); } pegar (errar) { if (errar instância de ValidationError) { throw new ReadError("Erro de validação", err); } outro { lançar errar; } } } tentar { readUser('{json ruim}'); } pegar (e) { if (e instância de ReadError) { alerta(e); // Erro original: SyntaxError: Token b inesperado em JSON na posição 1 alert("Erro original: " + e.causa); } outro { jogue e; } }
No código acima, readUser
funciona exatamente como descrito – captura erros de sintaxe e validação e, em vez disso, lança erros ReadError
(erros desconhecidos são relançados como de costume).
Portanto, o código externo verifica instanceof ReadError
e pronto. Não há necessidade de listar todos os tipos de erros possíveis.
A abordagem é chamada de “encapsulamento de exceções”, porque pegamos exceções de “baixo nível” e as “envolvemos” em ReadError
que é mais abstrato. É amplamente utilizado em programação orientada a objetos.
Podemos herdar de Error
e de outras classes de erro integradas normalmente. Só precisamos cuidar da propriedade name
e não esquecer de ligar super
.
Podemos usar instanceof
para verificar erros específicos. Também funciona com herança. Mas às vezes temos um objeto de erro vindo de uma biblioteca de terceiros e não há uma maneira fácil de obter sua classe. Então a propriedade name
pode ser usada para tais verificações.
O empacotamento de exceções é uma técnica difundida: uma função lida com exceções de baixo nível e cria erros de nível superior em vez de vários erros de baixo nível. Exceções de baixo nível às vezes se tornam propriedades desse objeto como err.cause
nos exemplos acima, mas isso não é estritamente necessário.
importância: 5
Crie uma classe FormatError
que herda da classe interna SyntaxError
.
Deve suportar propriedades message
, name
e stack
.
Exemplo de uso:
let err = new FormatError("erro de formatação"); alerta(err.mensagem); //erro de formatação alerta(err.nome); //Erro de formato alerta(err.stack); // pilha alerta (errar instância de FormatError); // verdadeiro alerta (errar instância de SyntaxError); // verdadeiro (porque herda de SyntaxError)
class FormatError estende SyntaxError { construtor(mensagem) { super(mensagem); este.nome = este.construtor.nome; } } let err = new FormatError("erro de formatação"); alerta(err.mensagem); //erro de formatação alerta(err.nome); //Erro de formato alerta(err.stack); // pilha alerta (errar instância de SyntaxError); // verdadeiro