Guía de manejo de errores de JavaScript

Los errores son buenos. El autor del material, cuya traducción publicamos hoy, dice que confía en que esta idea sea conocida por todos. A primera vista, los errores parecen atemorizantes. Pueden estar acompañados de algún tipo de pérdida. Un error cometido en público perjudica la autoridad de quien lo cometió. Pero al cometer errores, aprendemos de ellos, lo que significa que la próxima vez que nos encontremos en una situación en la que previamente nos comportamos incorrectamente, hacemos todo lo que sea necesario.



Arriba hablamos sobre los errores que cometen las personas en la vida cotidiana. Los errores en la programación son otra cosa. Los mensajes de error nos ayudan a mejorar el código, nos permiten informar a los usuarios de nuestros proyectos que algo salió mal y tal vez decirles a los usuarios cómo comportarse para que ya no se produzcan errores.

Este material de manejo de errores de JavaScript se divide en tres partes. Primero, daremos una descripción general del sistema de manejo de errores en JavaScript y hablaremos sobre los objetos de error. Después de eso, buscamos la respuesta a la pregunta de qué hacer con los errores que ocurren en el código del servidor (en particular, cuando se usa el paquete Node.js + Express.js). A continuación, discutimos el manejo de errores en React.js. Los marcos que se considerarán aquí se seleccionan debido a su enorme popularidad. Sin embargo, los principios para trabajar con los errores discutidos aquí son universales, por lo que incluso si no usa Express y React, puede aplicar fácilmente lo que aprendió a las herramientas con las que trabaja.

El código para el proyecto de demostración utilizado en este material se puede encontrar en este repositorio.

1. Errores en JavaScript y formas universales de trabajar con ellos.


Si algo salió mal en su código, puede usar la siguiente construcción.

throw new Error('something went wrong') 

Durante la ejecución de este comando, se creará una instancia del objeto Error y se generará una excepción (o, como dicen, "arrojada") con este objeto. La instrucción throw puede lanzar excepciones que contienen expresiones arbitrarias. En este caso, la ejecución del script se detendrá si no se han tomado medidas para manejar el error.

Los programadores principiantes de JS generalmente no usan la instrucción throw . Por lo general, se encuentran con excepciones lanzadas por el tiempo de ejecución del lenguaje o las bibliotecas de terceros. Cuando esto sucede, algo así como un ReferenceError: fs is not defined entra en la consola ReferenceError: fs is not defined y la ejecución del programa se detiene.

▍ Error de objeto


Las instancias del objeto Error tienen varias propiedades que podemos usar. La primera propiedad que nos interesa es el message . Aquí es donde se obtiene la línea que se puede pasar al constructor de errores como argumento. Por ejemplo, a continuación se muestra cómo crear una instancia del objeto Error y enviar a la consola la cadena que pasó el constructor accediendo a su propiedad de message .

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

La segunda propiedad del objeto, muy importante, es la traza de la pila de errores. Esta es una propiedad de stack . En cuanto a él, puede ver la pila de llamadas (historial de errores), que muestra la secuencia de operaciones que condujeron al mal funcionamiento del programa. En particular, esto nos permite comprender qué archivo contiene el código incorrecto y ver qué secuencia de llamadas a funciones condujo al error. Aquí hay un ejemplo de lo que puede ver accediendo a la propiedad de 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) 

Aquí, en la parte superior, hay un mensaje de error, seguido de una indicación de la sección de código cuya ejecución causó el error, luego describe el lugar desde donde se llamó a esta sección fallida. Esto continúa hasta el "más alejado" en relación con el fragmento de código de error.

▍ Generación y manejo de errores


Crear una instancia del objeto Error , es decir, ejecutar un comando de la forma new Error() , no conlleva ninguna consecuencia especial. Cosas interesantes comienzan a suceder después de aplicar el operador de throw , lo que genera un error. Como ya se mencionó, si dicho error no se procesa, la ejecución del script se detendrá. En este caso, no importa si el programador usó el operador throw , si ocurrió un error en cierta biblioteca o en el tiempo de ejecución del idioma (en un navegador o en Node.js). Hablemos de varios escenarios de manejo de errores.

▍construcción intente ... atrapar


El try...catch es la forma más fácil de manejar errores que a menudo se olvidan. Hoy en día, sin embargo, se usa mucho más intensamente que antes, debido al hecho de que puede usarse para manejar errores en construcciones async/await .

Este bloque se puede usar para manejar cualquier error que ocurra en el código síncrono. Considera un ejemplo.

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

Si en este ejemplo no incluimos el comando console.log(b) fallido en un try...catch , el script se detendría.

▍ finalmente bloquear


A veces sucede que algunos códigos deben ejecutarse independientemente de si se produjo un error o no. Para hacer esto, puede usar el tercer bloque opcional, finally , en el try...catch construct. A menudo, su uso es equivalente a algún código que viene inmediatamente después de try...catch , pero en algunas situaciones puede ser útil. Aquí hay un ejemplo de su uso.

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

▍Mecanismos asincrónicos - devoluciones de llamada


Al programar en JavaScript, siempre debe prestar atención a las secciones de código que se ejecutan de forma asincrónica. Si tiene una función asincrónica y se produce un error, la secuencia de comandos continuará ejecutándose. Cuando se implementan mecanismos asincrónicos en JS utilizando devoluciones de llamada (por cierto, esto no se recomienda), la devolución de llamada correspondiente (función de devolución de llamada) generalmente recibe dos parámetros. Esto es algo así como el parámetro err , que puede contener un error y un result , con los resultados de una operación asincrónica. Se parece a esto:

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

Si se produce un error en la devolución de llamada, es visible allí como un parámetro de err . De lo contrario, este parámetro obtendrá el valor undefined o null . Si resulta que err algo, es importante responder a esto porque en nuestro ejemplo, usando el comando return , o usando la if...else y colocando comandos en el bloque else para trabajar con el resultado de la operación asincrónica. El punto es, en el caso de que ocurra un error, excluir la posibilidad de trabajar con el resultado, el parámetro result , que en este caso puede ser undefined . Trabajar con este valor, si se supone, por ejemplo, que contiene un objeto, puede causar un error. Digamos que esto sucede cuando intentas usar la construcción result.data o result.data similar.

▍Mecanismos asincrónicos: promesas


Para realizar operaciones asincrónicas en JavaScript, es mejor usar promesas en lugar de devoluciones de llamada. Aquí, además de mejorar la legibilidad del código, existen mecanismos más avanzados para el manejo de errores. Es decir, no necesita preocuparse por el objeto de error que puede caer en la función de devolución de llamada al usar promesas. Aquí para este propósito se proporciona un bloque de catch especial. Intercepta todos los errores que ocurrieron en las promesas anteriores o todos los errores que ocurrieron en el código después del catch anterior. Tenga en cuenta que si se produce un error en la promesa que no tiene un bloque catch para procesar, esto no detendrá la ejecución del script, pero el mensaje de error no será particularmente legible.

 (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, siempre puede recomendar, cuando trabaje con promesas, usar el catch . Echa un vistazo a un ejemplo.

 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 asincrónicos e intenta ... atrapar


Después de que apareciera la construcción async/await en JavaScript, volvimos a la forma clásica de manejar los errores: try...catch...finally . Manejar errores con este enfoque es muy fácil y conveniente. Considera un ejemplo.

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

Con este enfoque, los errores en el código asincrónico se manejan de la misma manera que en el síncrono. Como resultado, ahora, si es necesario, en un solo catch puede manejar una gama más amplia de errores.

2. Generando y procesando errores en el código del servidor


Ahora que tenemos las herramientas para trabajar con errores, veamos qué podemos hacer con ellos en situaciones reales. Generar y manejar correctamente los errores es un aspecto crítico de la programación del lado del servidor. Existen diferentes enfoques para trabajar con errores. Aquí demostraremos un enfoque utilizando nuestro propio constructor para instancias del objeto Error y códigos de error que se pasan convenientemente al front-end o a cualquier mecanismo que use API de servidor. La forma en que se estructura el backend de un proyecto específico realmente no importa, ya que con cualquier enfoque puede usar las mismas ideas para trabajar con errores.

Como el marco del servidor responsable del enrutamiento, utilizaremos Express.js. Pensemos en qué estructura necesitamos para organizar un sistema eficaz de manejo de errores. Entonces, esto es lo que necesitamos:

  1. El manejo universal de errores es un mecanismo básico adecuado para manejar cualquier error, durante el cual Something went wrong, please try again or contact us un mensaje como Something went wrong, please try again or contact us , pidiéndole al usuario que intente realizar la operación que falló, nuevamente o comuníquese con el propietario del servidor. Este sistema no es particularmente inteligente, pero al menos puede informar al usuario de que algo salió mal. Tal mensaje es mucho mejor que la "descarga sin fin" o algo similar.
  2. Procesamiento de errores específicos: un mecanismo que le permite informar al usuario información detallada sobre las causas del comportamiento incorrecto del sistema y brindarle consejos específicos sobre cómo abordar el problema. Por ejemplo, esto puede referirse a la ausencia de algunos datos importantes en la solicitud que el usuario envía al servidor, o que ya hay un cierto registro en la base de datos que está tratando de agregar nuevamente, y así sucesivamente.

▍ Desarrollando su propio constructor de objetos de error


Aquí usaremos la clase de Error estándar y la extenderemos. El uso de mecanismos de herencia en JavaScript es arriesgado, pero en este caso, estos mecanismos son muy útiles. ¿Por qué necesitamos herencia? El hecho es que para que podamos depurar convenientemente el código, necesitamos información sobre el seguimiento de la pila de errores. Extendiendo la clase de Error estándar, tenemos la capacidad de rastrear la pila sin esfuerzo adicional. Agregamos dos propiedades a nuestro propio objeto de error. La primera es la propiedad de code , a la que se puede acceder utilizando una estructura de la forma err.code . El segundo es la propiedad de status . Grabará el código de estado HTTP, que se planea transmitir a la parte cliente de la aplicación.

Así es como se ve la clase CustomError , cuyo código está diseñado como un 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 

▍ Enrutamiento


Ahora que nuestro objeto de error está listo para su uso, necesitamos configurar la estructura de ruta. Como se mencionó anteriormente, necesitamos implementar un enfoque unificado para el manejo de errores, que nos permite manejar igualmente los errores para todas las rutas. Por defecto, el framework Express.js no es totalmente compatible con dicho esquema. El hecho es que todas sus rutas están encapsuladas.

Para hacer frente a este problema, podemos implementar nuestro propio controlador de ruta y definir la lógica de ruta en forma de funciones ordinarias. Gracias a este enfoque, si la función de ruta (o cualquier otra función) arroja un error, caerá en el controlador de ruta, que luego puede pasarlo a la parte cliente de la aplicación. Si se produce un error en el servidor, planeamos transferirlo al front-end en el siguiente formato, suponiendo que la API JSON se utilizará para esto:

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

Si en esta etapa lo que está sucediendo le parece incomprensible, no se preocupe, solo siga leyendo, intente trabajar con lo que se está discutiendo y gradualmente lo resolverá. De hecho, si hablamos de capacitación en informática, el enfoque "de arriba hacia abajo" se usa aquí, cuando las ideas generales se discuten por primera vez y luego se lleva a cabo la transición a detalles.

Así es como se ve el código del controlador de ruta.

 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 

Creemos que los comentarios en el código lo explican bastante bien. Esperamos que leerlos sea más conveniente que las explicaciones de dicho código dadas después.

Ahora eche un vistazo al archivo de ruta.

 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, } 

En estos ejemplos, no se hace nada con las consultas mismas. Simplemente considera diferentes escenarios para la ocurrencia de errores. Entonces, por ejemplo, la solicitud GET /city caerá en la función const GET = req =>... , la POST /city caerá en la función const POST = req =>... y así sucesivamente. Este esquema también funciona cuando se utilizan parámetros de consulta. Por ejemplo, para una solicitud del formulario GET /city?startsWith=R En general, se ha demostrado aquí que al procesar errores, la interfaz de usuario puede recibir un error general que contiene solo una oferta para volver a intentarlo o contactar al propietario del servidor, o un error generado utilizando el constructor CustomError que contiene información detallada sobre el problema.
Los datos de error general llegarán a la parte del cliente de la aplicación en el siguiente formulario:

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

El constructor CustomError se usa así:

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

Esto proporciona el siguiente código JSON pasado a la interfaz:

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

Ahora que hemos trabajado a fondo en la parte del servidor de la aplicación, los registros de errores inútiles ya no caen en la parte del cliente. En cambio, el cliente recibe información útil sobre lo que salió mal.

No olvide que aquí se encuentra el repositorio con el código considerado aquí. Puede descargarlo, experimentar con él y, si es necesario, adaptarlo a las necesidades de su proyecto.

3. Trabajar con errores en el cliente.


Ahora es el momento de describir la tercera parte de nuestro sistema de manejo de errores front-end. Aquí será necesario, en primer lugar, manejar los errores que ocurren en la parte cliente de la aplicación, y en segundo lugar, será necesario notificar al usuario sobre los errores que ocurren en el servidor. Primero trataremos con mostrar información de error del servidor. Como ya se mencionó, la biblioteca React se usará en este ejemplo.

▍Guardar información de error en el estado de la aplicación


Al igual que cualquier otro dato, los errores y los mensajes de error pueden cambiar, por lo que tiene sentido colocarlos en el estado de los componentes. Cuando se monta el componente, los datos de error se restablecen, por lo tanto, cuando el usuario ve por primera vez la página, no habrá mensajes de error.

El siguiente problema es que los errores del mismo tipo deben mostrarse con el mismo estilo. Por analogía con el servidor, aquí puede distinguir 3 tipos de errores.

  1. Errores globales: esta categoría incluye mensajes de error de naturaleza general que provienen del servidor o errores que, por ejemplo, ocurren si el usuario no ha iniciado sesión en el sistema en otras situaciones similares.
  2. Errores específicos generados por el lado del servidor de la aplicación: esto incluye errores que se informan desde el servidor. Por ejemplo, se produce un error similar si un usuario intentó iniciar sesión y envió un nombre de usuario y contraseña al servidor, y el servidor le informó que la contraseña era incorrecta. Tales cosas no se verifican en la parte del cliente de la aplicación, por lo que los mensajes sobre dichos errores deben provenir del servidor.
  3. Errores específicos generados por la parte cliente de la aplicación. Un ejemplo de dicho error es un mensaje sobre una dirección de correo electrónico no válida ingresada en el campo correspondiente.

Los errores del segundo y tercer tipo son muy similares, puede trabajar con ellos utilizando el almacén de estado de componentes del mismo nivel. Su principal diferencia es que provienen de diferentes fuentes. A continuación, analizando el código, veremos cómo trabajar con ellos.

Utilizará el sistema incorporado para administrar el estado de la aplicación en React, pero, si es necesario, puede usar soluciones especializadas para administrar el estado, como MobX o Redux.

▍ Errores globales


Típicamente, tales mensajes de error se almacenan en el componente de nivel más alto con un estado. Se muestran en un elemento de interfaz de usuario estático. Puede ser un cuadro rojo en la parte superior de la pantalla, una ventana modal o cualquier otra cosa. La implementación depende del proyecto específico. Así es como se ve el mensaje de error.


Mensaje de error global

Ahora eche un vistazo al código que está almacenado en el archivo 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 ), , . , , , , , , , , . .

Resumen


Esperamos que ahora comprenda cómo lidiar con los errores en las aplicaciones web. Algo como esto console.error(err)debería usarse solo para propósitos de depuración, tales cosas olvidadas por el programador no deberían penetrar en la producción. Simplifica la solución al problema de iniciar sesión utilizando alguna biblioteca adecuada como loglevel .

Estimados lectores! ¿Cómo manejas los errores en tus proyectos?

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


All Articles