Guia de tratamento de erros JavaScript

Erros são bons. O autor do material, cuja tradução publicamos hoje, afirma estar confiante de que essa idéia é conhecida por todos. À primeira vista, os erros parecem assustadores. Eles podem ser acompanhados por algum tipo de perda. Um erro cometido em público prejudica a autoridade de quem o cometeu. Mas, cometendo erros, aprendemos com eles, o que significa que da próxima vez que entrarmos em uma situação em que anteriormente nos comportamos incorretamente, faremos tudo que for necessário.



Acima, falamos sobre os erros que as pessoas cometem na vida cotidiana. Erros na programação são outra coisa. As mensagens de erro nos ajudam a melhorar o código, permitem informar aos usuários de nossos projetos que algo deu errado e possivelmente informam aos usuários como se comportar para que os erros não ocorram mais.

Este material de tratamento de erros JavaScript é dividido em três partes. Primeiro, forneceremos uma visão geral do sistema de tratamento de erros em JavaScript e falaremos sobre objetos de erro. Depois disso, procuramos a resposta para a pergunta sobre o que fazer com os erros que ocorrem no código do servidor (em particular, ao usar o pacote Node.js + Express.js). Em seguida, discutiremos o tratamento de erros no React.js. As estruturas que serão consideradas aqui são selecionadas devido à sua enorme popularidade. No entanto, os princípios para trabalhar com os erros discutidos aqui são universais; portanto, mesmo se você não usar o Express and React, poderá aplicar facilmente o que aprendeu às ferramentas com as quais trabalha.

O código para o projeto de demonstração usado neste material pode ser encontrado neste repositório.

1. Erros no JavaScript e formas universais de trabalhar com eles


Se algo der errado no seu código, você poderá usar a seguinte construção.

throw new Error('something went wrong') 

Durante a execução deste comando, uma instância do objeto Error será criada e uma exceção será gerada (ou, como se costuma dizer, "lançada") com esse objeto. A instrução throw pode lançar exceções que contêm expressões arbitrárias. Nesse caso, a execução do script será interrompida se não forem tomadas medidas para lidar com o erro.

Os programadores iniciantes em JS geralmente não usam a instrução throw . Eles geralmente encontram exceções geradas pelo tempo de execução do idioma ou pelas bibliotecas de terceiros. Quando isso acontece, algo como um ReferenceError: fs is not defined entra no console ReferenceError: fs is not defined e a execução do programa para.

Erro de objeto


Instâncias do objeto Error possuem várias propriedades que podemos usar. A primeira propriedade em que estamos interessados ​​é a message . É aqui que a linha obtém que pode ser passada para o construtor de erros como argumento. Por exemplo, o seguinte mostra como instanciar o objeto Error e enviar para o console a string transmitida pelo construtor, acessando sua propriedade message .

 const myError = new Error('please improve your code') console.log(myError.message) // please improve your code 

A segunda propriedade do objeto, muito importante, é o rastreamento da pilha de erros. Esta é uma propriedade de stack . Em relação a isso, é possível visualizar a pilha de chamadas (histórico de erros), que mostra a sequência de operações que levaram ao mau funcionamento do programa. Em particular, isso nos permite entender qual arquivo contém o código incorreto e ver qual sequência de chamadas de função levou ao erro. Aqui está um exemplo do que você pode ver acessando a propriedade stack .

 Error: please improve your code at Object.<anonymous> (/Users/gisderdube/Documents/_projects/hacking.nosync/error-handling/src/general.js:1:79) at Module._compile (internal/modules/cjs/loader.js:689:30) at Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10) at Module.load (internal/modules/cjs/loader.js:599:32) at tryModuleLoad (internal/modules/cjs/loader.js:538:12) at Function.Module._load (internal/modules/cjs/loader.js:530:3) at Function.Module.runMain (internal/modules/cjs/loader.js:742:12) at startup (internal/bootstrap/node.js:266:19) at bootstrapNodeJSCore (internal/bootstrap/node.js:596:3) 

Aqui, na parte superior, há uma mensagem de erro, seguida por uma indicação da seção de código cuja execução causou o erro, e descreve o local de onde essa seção com falha foi chamada. Isso continua no "mais distante" em relação ao fragmento do código de erro.

▍ Geração e tratamento de erros


Criar uma instância do objeto Error , ou seja, executar um comando no formato new Error() , não leva a consequências especiais. Coisas interessantes começam a acontecer após a aplicação do operador throw , o que gera um erro. Como já mencionado, se esse erro não for processado, a execução do script será interrompida. Nesse caso, não faz diferença se o operador throw foi usado pelo próprio programador, se ocorreu um erro em uma determinada biblioteca ou no tempo de execução do idioma (em um navegador ou no Node.js.). Vamos falar sobre vários cenários de tratamento de erros.

Tryconstruction try ... catch


O try...catch é a maneira mais fácil de lidar com erros frequentemente esquecidos. Atualmente, no entanto, é usado com muito mais intensidade do que antes, devido ao fato de poder ser usado para manipular erros em construções async/await .

Este bloco pode ser usado para manipular quaisquer erros que ocorram no código síncrono. Considere um exemplo.

 const a = 5 try {   console.log(b) //  b   -   } catch (err) {   console.error(err) //          } console.log(a) //    ,    

Se neste exemplo não incluímos o comando console.log(b) falha em um try...catch , o script seria interrompido.

Block finalmente bloqueia


Às vezes, acontece que algum código precisa ser executado, independentemente de ocorrer um erro ou não. Para fazer isso, você pode usar o terceiro bloco, opcional, finally , na construção try...catch . Freqüentemente, seu uso é equivalente a algum código que vem imediatamente após a try...catch , mas em algumas situações pode ser útil. Aqui está um exemplo de seu uso.

 const a = 5 try {   console.log(b) //  b   -   } catch (err) {   console.error(err) //          } finally {   console.log(a) //        } 

Mecanismos assíncronos - retornos de chamada


Ao programar em JavaScript, você deve sempre prestar atenção às seções de código que são executadas de forma assíncrona. Se você tiver uma função assíncrona e ocorrer um erro, o script continuará sendo executado. Quando mecanismos assíncronos em JS são implementados usando retornos de chamada (a propósito, isso não é recomendado), o retorno de chamada correspondente (função de retorno de chamada) geralmente recebe dois parâmetros. É algo como o parâmetro err , que pode conter um erro e result - com os resultados de uma operação assíncrona. Parece algo como isto:

 myAsyncFunc(someInput, (err, result) => {   if(err) return console.error(err) //           console.log(result) }) 

Se ocorrer um erro no retorno de chamada, ele será visível como um parâmetro err . Caso contrário, esse parâmetro obterá o valor undefined ou null . Se acontecer que err algo, é importante responder a isso, porque no nosso exemplo, usando o comando return ou usando a if...else e colocando comandos no bloco else para trabalhar com o resultado da operação assíncrona. A questão é excluir, no caso de um erro, a possibilidade de trabalhar com o resultado, o parâmetro result , que neste caso pode ser undefined . Trabalhar com esse valor, se for assumido, por exemplo, que ele contém um objeto, pode causar um erro. Digamos que isso aconteça quando você tenta usar a construção result.data ou result.data semelhante a ela.

Mecanismos assíncronos - promessas


Para executar operações assíncronas em JavaScript, é melhor usar promessas do que retornos de chamada. Aqui, além da legibilidade aprimorada do código, existem mecanismos mais avançados de tratamento de erros. Ou seja, você não precisa se preocupar com o objeto de erro que pode cair na função de retorno de chamada ao usar promessas. Aqui, para esse fim, é fornecido um bloco de catch especial. Ele intercepta todos os erros que ocorreram nas promessas anteriores ou todos os erros que ocorreram no código após o catch anterior. Observe que, se ocorrer um erro na promessa que não possui um bloco de catch para processar, isso não interromperá a execução do script, mas a mensagem de erro não será particularmente legível.

 (node:7741) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: something went wrong (node:7741) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code. */ 

Como resultado, você sempre pode recomendar, ao trabalhar com promessas, usar o catch . Veja um exemplo.

 Promise.resolve(1)   .then(res => {       console.log(res) // 1       throw new Error('something went wrong')       return Promise.resolve(2)   })   .then(res => {       console.log(res) //        })   .catch(err => {       console.error(err) //  ,     ,         return Promise.resolve(3)   })   .then(res => {       console.log(res) // 3   })   .catch(err => {       //      ,      -        console.error(err)   }) 

Mecanismos assíncronos e tente ... pegar


Depois que a construção async/await apareceu em JavaScript, voltamos à maneira clássica de lidar com erros - para try...catch...finally . Lidar com erros com essa abordagem é muito fácil e conveniente. Considere um exemplo.

 ;(async function() {   try {       await someFuncThatThrowsAnError()   } catch (err) {       console.error(err) //       }   console.log('Easy!') //   })() 

Com essa abordagem, os erros no código assíncrono são tratados da mesma maneira que no síncrono. Como resultado, agora, se necessário, em um único catch você pode lidar com uma ampla gama de erros.

2. Gerando e Processando Erros no Código do Servidor


Agora que temos as ferramentas para trabalhar com erros, vejamos o que podemos fazer com eles em situações reais. Gerar e manipular corretamente erros é um aspecto crítico da programação do lado do servidor. Existem diferentes abordagens para trabalhar com erros. Aqui, demonstraremos uma abordagem usando nosso próprio construtor para instâncias do objeto Error e códigos de erro que são convenientemente passados ​​para o front-end ou para qualquer mecanismo que utilize APIs de servidor. Como o backend de um projeto específico é estruturado não importa, pois com qualquer abordagem, você pode usar as mesmas idéias para trabalhar com erros.

Como estrutura do servidor responsável pelo roteamento, usaremos o Express.js. Vamos pensar em qual estrutura precisamos para organizar um sistema eficaz de tratamento de erros. Então, aqui está o que precisamos:

  1. O tratamento universal de erros é um mecanismo básico adequado para o tratamento de erros, durante os quais uma mensagem como Something went wrong, please try again or contact us , solicitando ao usuário que tente executar a operação que falhou novamente ou entre em contato com o proprietário do servidor. Este sistema não é particularmente inteligente, mas pelo menos é capaz de informar ao usuário que algo deu errado. Essa mensagem é muito melhor do que "download sem fim" ou algo semelhante.
  2. Processando erros específicos - um mecanismo que permite informar ao usuário informações detalhadas sobre as causas do comportamento inadequado do sistema e fornecer conselhos específicos sobre como lidar com o problema. Por exemplo, isso pode estar relacionado à ausência de alguns dados importantes na solicitação que o usuário envia ao servidor ou se já existe um certo registro no banco de dados que ele está tentando adicionar novamente e assim por diante.

▍ Desenvolvendo seu próprio construtor de objetos de erro


Aqui vamos usar a classe Error padrão e estendê-la. O uso de mecanismos de herança em JavaScript é arriscado, mas nesse caso, esses mecanismos são muito úteis. Por que precisamos de herança? O fato é que, para podermos depurar convenientemente o código, precisamos de informações sobre o rastreamento da pilha de erros. Estendendo a classe Error padrão, temos a capacidade de rastrear a pilha sem esforço extra. Adicionamos duas propriedades ao nosso próprio objeto de erro. A primeira é a propriedade code , que pode ser acessada usando uma estrutura no formato err.code . O segundo é a propriedade status . Ele gravará o código de status HTTP, que está planejado para ser transmitido à parte do cliente do aplicativo.

É assim que a classe CustomError parece, cujo código é projetado como um módulo.

 class CustomError extends Error {   constructor(code = 'GENERIC', status = 500, ...params) {       super(...params)       if (Error.captureStackTrace) {           Error.captureStackTrace(this, CustomError)       }       this.code = code       this.status = status   } } module.exports = CustomError 

▍ Roteamento


Agora que nosso objeto de erro está pronto para uso, precisamos configurar a estrutura da rota. Como mencionado acima, precisamos implementar uma abordagem unificada para o tratamento de erros, o que nos permite lidar igualmente com erros em todas as rotas. Por padrão, a estrutura Express.js. não oferece suporte completo a esse esquema. O fato é que todas as suas rotas são encapsuladas.

Para lidar com esse problema, podemos implementar nosso próprio manipulador de rotas e definir a lógica da rota na forma de funções comuns. Graças a essa abordagem, se a função de rota (ou qualquer outra função) gerar um erro, ela cairá no manipulador de rotas, que poderá transmiti-la à parte do cliente do aplicativo. Se ocorrer um erro no servidor, planejamos transferi-lo para o front-end no seguinte formato, assumindo que a API JSON será usada para isso:

 {   error: 'SOME_ERROR_CODE',   description: 'Something bad happened. Please try again or contact support.' } 

Se, nesta fase, o que está acontecendo lhe parecer incompreensível - não se preocupe - continue lendo, tente trabalhar com o que está sendo discutido e, gradualmente, você descobrirá. De fato, se falamos sobre treinamento em informática, a abordagem “de cima para baixo” é usada aqui, quando idéias gerais são discutidas pela primeira vez e, em seguida, a transição para detalhes é realizada.

É assim que o código do manipulador de rota se parece.

 const express = require('express') const router = express.Router() const CustomError = require('../CustomError') router.use(async (req, res) => {   try {       const route = require(`.${req.path}`)[req.method]       try {           const result = route(req) //    route           res.send(result) //   ,     route       } catch (err) {           /*                ,    route             */           if (err instanceof CustomError) {               /*                   -                                  */               return res.status(err.status).send({                   error: err.code,                   description: err.message,               })           } else {               console.error(err) //                  //   -                   return res.status(500).send({                   error: 'GENERIC',                   description: 'Something went wrong. Please try again or contact support.',               })           }       }   } catch (err) {       /*          ,    ,  ,            ,  ,          ,                       */       res.status(404).send({           error: 'NOT_FOUND',           description: 'The resource you tried to access does not exist.',       })   } }) module.exports = router 

Acreditamos que os comentários no código explicam isso muito bem. Esperamos que lê-los seja mais conveniente do que as explicações desse código fornecidas depois dele.

Agora dê uma olhada no arquivo de rota.

 const CustomError = require('../CustomError') const GET = req => {   //       return { name: 'Rio de Janeiro' } } const POST = req => {   //       throw new Error('Some unexpected error, may also be thrown by a library or the runtime.') } const DELETE = req => {   //  ,      throw new CustomError('CITY_NOT_FOUND', 404, 'The city you are trying to delete could not be found.') } const PATCH = req => {   //      CustomError   try {       //   -        throw new Error('Some internal error')   } catch (err) {       console.error(err) //    ,           throw new CustomError(           'CITY_NOT_EDITABLE',           400,           'The city you are trying to edit is not editable.'       )   } } module.exports = {   GET,   POST,   DELETE,   PATCH, } 

Nestes exemplos, nada é feito com as próprias consultas. Simplesmente considera diferentes cenários para a ocorrência de erros. Assim, por exemplo, a solicitação GET /city cairá na função const GET = req =>... , a solicitação POST /city cairá na função const POST = req =>... e assim por diante. Esse esquema também funciona ao usar parâmetros de consulta. Por exemplo, para uma solicitação no formato GET /city?startsWith=R Em geral, foi demonstrado aqui que, ao processar erros, o front-end pode receber um erro geral que contém apenas uma oferta para tentar novamente ou entrar em contato com o proprietário do servidor ou um erro gerado usando o construtor CustomError que contém informações detalhadas sobre o problema.
Os dados gerais de erro chegarão à parte do cliente do aplicativo da seguinte forma:

 {   error: 'GENERIC',   description: 'Something went wrong. Please try again or contact support.' } 

O construtor CustomError é usado assim:

 throw new CustomError('MY_CODE', 400, 'Error description') 

Isso fornece o seguinte código JSON passado para o front-end:

 {   error: 'MY_CODE',   description: 'Error description' } 

Agora que trabalhamos completamente na parte do servidor do aplicativo, os logs de erros inúteis não caem mais na parte do cliente. Em vez disso, o cliente recebe informações úteis sobre o que deu errado.

Não esqueça que aqui reside o repositório com o código considerado aqui. Você pode fazer o download, experimentar e, se necessário, adaptá-lo às necessidades do seu projeto.

3. Trabalhe com erros no cliente


Agora é hora de descrever a terceira parte do nosso sistema de tratamento de erros de front-end. Aqui será necessário, em primeiro lugar, lidar com os erros que ocorrem na parte do cliente do aplicativo e, em segundo lugar, será necessário notificar o usuário sobre os erros que ocorrem no servidor. Primeiro, lidaremos com a exibição de informações de erro do servidor. Como já mencionado, a biblioteca React será usada neste exemplo.

Salvar informações de erro no estado do aplicativo


Como qualquer outro dado, os erros e as mensagens de erro podem mudar, por isso faz sentido colocá-los no estado dos componentes. Quando o componente é montado, os dados do erro são redefinidos; portanto, quando o usuário vê a página pela primeira vez, não haverá mensagens de erro.

A próxima coisa a ser resolvida é que erros do mesmo tipo precisam ser mostrados no mesmo estilo. Por analogia com o servidor, aqui você pode distinguir três tipos de erros.

  1. Erros globais - essa categoria inclui mensagens de erro de natureza geral provenientes do servidor ou erros que, por exemplo, ocorrem se o usuário não estiver conectado ao sistema em outras situações semelhantes.
  2. Erros específicos gerados pelo lado do servidor do aplicativo - isso inclui erros que são relatados pelo servidor. Por exemplo, um erro semelhante ocorre se um usuário tentar efetuar login e enviar um nome de usuário e senha ao servidor, e o servidor o informou que a senha estava incorreta. Como essas coisas não são verificadas na parte do cliente do aplicativo, as mensagens sobre esses erros devem vir do servidor.
  3. Erros específicos gerados pela parte do cliente do aplicativo. Um exemplo desse erro é uma mensagem sobre um endereço de email inválido inserido no campo correspondente.

Os erros do segundo e terceiro tipos são muito semelhantes; você pode trabalhar com eles usando o armazenamento de estado de componentes do mesmo nível. A principal diferença é que eles vêm de diferentes fontes. Abaixo, analisando o código, veremos como trabalhar com eles.

Ele usará o sistema interno para gerenciar o estado do aplicativo no React, mas, se necessário, você pode usar soluções especializadas para gerenciar o estado - como MobX ou Redux.

Errors Erros globais


Normalmente, essas mensagens de erro são armazenadas no componente de nível mais alto com um estado. Eles são exibidos em um elemento estático da interface do usuário. Pode ser uma caixa vermelha na parte superior da tela, uma janela modal ou qualquer outra coisa. A implementação depende do projeto específico. É assim que a mensagem de erro se parece.


Mensagem de erro global

Agora, veja o código armazenado no arquivo Application.js .

 import React, { Component } from 'react' import GlobalError from './GlobalError' class Application extends Component {   constructor(props) {       super(props)       this.state = {           error: '',       }       this._resetError = this._resetError.bind(this)       this._setError = this._setError.bind(this)   }   render() {       return (           <div className="container">               <GlobalError error={this.state.error} resetError={this._resetError} />               <h1>Handling Errors</h1>           </div>       )   }   _resetError() {       this.setState({ error: '' })   }   _setError(newError) {       this.setState({ error: newError })   } } export default Application 

, , Application.js , . , .

GlobalError , x , . GlobalError ( GlobalError.js ).

 import React, { Component } from 'react' class GlobalError extends Component {   render() {       if (!this.props.error) return null       return (           <div               style={{                   position: 'fixed',                   top: 0,                   left: '50%',                   transform: 'translateX(-50%)',                   padding: 10,                   backgroundColor: '#ffcccc',                   boxShadow: '0 3px 25px -10px rgba(0,0,0,0.5)',                   display: 'flex',                   alignItems: 'center',               }}           >               {this.props.error}                              <i                   className="material-icons"                   style={{ cursor: 'pointer' }}                   onClick={this.props.resetError}               >                   close               </font></i>           </div>       )   } } export default GlobalError 

if (!this.props.error) return null . , . . , , , . , , x , - , .

, , _setError Application.js . , , , , ( error: 'GENERIC' ). ( GenericErrorReq.js ).

 import React, { Component } from 'react' import axios from 'axios' class GenericErrorReq extends Component {   constructor(props) {       super(props)       this._callBackend = this._callBackend.bind(this)   }   render() {       return (           <div>               <button onClick={this._callBackend}>Click me to call the backend</button>           </div>       )   }   _callBackend() {       axios           .post('/api/city')           .then(result => {               //  -     ,               })           .catch(err => {               if (err.response.data.error === 'GENERIC') {                   this.props.setError(err.response.data.description)               }           })   } } export default GenericErrorReq 

, . , , . . -, , -, UX-, , — .

▍ ,


, , , .




, . . . SpecificErrorReq.js .

 import React, { Component } from 'react' import axios from 'axios' import InlineError from './InlineError' class SpecificErrorRequest extends Component {   constructor(props) {       super(props)       this.state = {           error: '',       }       this._callBackend = this._callBackend.bind(this)   }   render() {       return (           <div>               <button onClick={this._callBackend}>Delete your city</button>               <InlineError error={this.state.error} />           </div>       )   }   _callBackend() {       this.setState({           error: '',       })       axios           .delete('/api/city')           .then(result => {               //  -     ,               })           .catch(err => {               if (err.response.data.error === 'GENERIC') {                   this.props.setError(err.response.data.description)               } else {                   this.setState({                       error: err.response.data.description,                   })               }           })   } } export default SpecificErrorRequest 

, , , x . , , . , , — , , , . , , . , , , — .

▍,


, , , . , , - . .


,

SpecificErrorFrontend.js , .

 import React, { Component } from 'react' import axios from 'axios' import InlineError from './InlineError' class SpecificErrorRequest extends Component {   constructor(props) {       super(props)       this.state = {           error: '',           city: '',       }       this._callBackend = this._callBackend.bind(this)       this._changeCity = this._changeCity.bind(this)   }   render() {       return (           <div>               <input                   type="text"                   value={this.state.city}                   style={{ marginRight: 15 }}                   onChange={this._changeCity}               />               <button onClick={this._callBackend}>Delete your city</button>               <InlineError error={this.state.error} />           </div>       )   }   _changeCity(e) {       this.setState({           error: '',           city: e.target.value,       })   }   _validate() {       if (!this.state.city.length) throw new Error('Please provide a city name.')   }   _callBackend() {       this.setState({           error: '',       })       try {           this._validate()       } catch (err) {           return this.setState({ error: err.message })       }       axios           .delete('/api/city')           .then(result => {               //  -     ,               })           .catch(err => {               if (err.response.data.error === 'GENERIC') {                   this.props.setError(err.response.data.description)               } else {                   this.setState({                       error: err.response.data.description,                   })               }           })   } } export default SpecificErrorRequest 


, , ( GENERIC ), , . , , , , , , , , . .

Sumário


, , -. console.error(err) , , , . - loglevel .

! ?

Source: https://habr.com/ru/post/pt431078/


All Articles