📜 ⬆️ ⬇️

Javascript error handling guide

Bugs are good. The author of the material, the translation of which we are publishing today, says that he is confident that this idea is known to all. At first glance, errors seem to be something scary. They may have some kind of loss. An error made in public harms the authority of the one who made it. But, making mistakes, we learn from them, which means that the next time we get into a situation in which we have behaved incorrectly, we do everything as it should.



Above, we talked about the mistakes that people make in ordinary life. Errors in programming are something else. Error messages help us improve the code, they allow us to inform users of our projects that something has gone wrong, and, perhaps, tell users how to behave so that errors do not occur anymore.

This JavaScript error handling material is divided into three parts. First we will give a general overview of the error handling system in JavaScript and talk about the error objects. After that, we will look for the answer to the question of what to do with errors that occur in the server code (in particular, when using the Node.js + Express.js bundle). Next, let's discuss error handling in React.js. The frameworks that will be considered here are chosen because of their immense popularity. However, the principles of working with errors that are considered here are universal, so you, even if you do not use Express and React, can easily apply what you have learned to the tools you work with.
')
The code for the demonstration project used in this material can be found in this repository.

1. Errors in JavaScript and universal ways of working with them


If something went wrong in your code, you can use the following construct.

throw new Error('something went wrong') 

During the execution of this command, an instance of the Error object will be created and an exception will be generated (or, as they say, “thrown out”) with this object. The throw statement can generate exceptions containing arbitrary expressions. At the same time, the execution of the script will stop if measures were not taken to handle the error.

Novice JS programmers usually do not use the throw statement. They, as a rule, are faced with exceptions issued by either the language runtime environment or third-party libraries. When this happens, something like ReferenceError: fs is not defined gets to the console ReferenceError: fs is not defined and the program execution stops.

ErrorProject Error


An Error object instance has several properties that we can use. The first property of interest is message . This is exactly the line that can be passed to the error constructor as an argument. For example, the following shows the creation of an instance of the Error object and the output to the console of a string passed by the constructor through a call to its message property.

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

The second property of the object, very important, is a trace of the error stack. This is a stack property. Referring to it, you can view the call stack (error history), which shows the sequence of operations that led to the incorrect operation of the program. In particular, it allows you to understand which file contains the failed code and see which sequence of function calls resulted in an error. Here is an example of what can be seen by accessing the stack property.

 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) 

Here, in the upper part, there is an error message, followed by an indication of the code section, the execution of which caused an error, then the place from which this bad section was called is described. This continues to the most "distant" in relation to the error code fragment.

â–Ť Generation and error handling


Creating an instance of the Error object, that is, executing a command like new Error() , does not lead to any special consequences. Interesting things start to happen after applying the throw operator, which generates an error. As already mentioned, if this error is not processed, the execution of the script will stop. In this case, there is no difference whether the throw statement was used by the programmer himself, whether an error occurred in some library or in the language runtime (in the browser or in Node.js). Let's talk about various error handling scenarios.

TryDesign try ... catch


The try...catch is the easiest way to handle errors, which is often forgotten. Nowadays, however, it is used much more intensively than before, due to the fact that it can be used to handle errors in async/await constructions.

This block can be used to handle any errors occurring in the synchronous code. Consider an example.

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

If, in this example, we did not enclose the failed console.log(b) command in a try...catch , the script would be stopped.

Finally finally block


Sometimes it happens that a code needs to be executed regardless of whether an error has occurred or not. To do this, in the try...catch construct, use the third, optional, block finally . Often, its use is equivalent to some kind of code that comes right after try...catch , but in some situations it can be useful. Here is an example of its use.

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

â–Ť Asynchronous mechanisms - callbacks


Programming in JavaScript is always worth paying attention to the code that runs asynchronously. If you have an asynchronous function and an error occurs in it, the script will continue to run. When asynchronous mechanisms in JS are implemented using callbacks (by the way, this is not recommended), the corresponding callback (callback function) usually receives two parameters. This is something like the err parameter, which may contain an error, and result with the results of an asynchronous operation. It looks like this:

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

If an error gets into the callback, it is visible there as an err parameter. Otherwise, this parameter will get the value undefined or null . If it turns out that there is something in err , it is important to respond to this, either as in our example, using the return command, or using the if...else and placing commands in the else block to work with the result of the asynchronous operation. The point is that, in the event that an error occurred, to exclude the possibility of working with the result, the parameter result , which in this case can be set to undefined . Working with this value, if it is assumed, for example, that it contains an object, can itself cause an error. Let's say it happens when you try to use a result.data construct or the like.

â–Ť Asynchronous mechanisms - promises


To perform asynchronous operations in JavaScript, it is better to use not callbacks but promises. Here, in addition to improved code readability, there are more advanced error handling mechanisms. Namely, there is no need to mess around with the object of the error that may fall into the callback function when using promises. Here a special catch block is provided for this purpose. It intercepts all errors that occurred in promises that are before it, or all errors that occurred in the code after the previous catch . Note that if there was an error in the promise that does not have a catch block for processing, this will not stop the execution of the script, but the error message will not be particularly readable.

 (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. */ 

As a result, you can always recommend, when working with promises, use the catch . Take a look at an example.

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

â–ŤAsynchronous mechanisms and try ... catch


After the async/await construction appeared in JavaScript, we returned to the classic error handling method - to try...catch...finally . Handling errors with this approach is very easy and convenient. Consider an example.

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

With this approach, errors in asynchronous code are handled in the same way as in synchronous. As a result, now, if necessary, in a single catch you can handle a wider range of errors.

2. Generation and error handling in server code


Now that we have the tools to work with errors, let's look at what we can do with them in real situations. Generating and correct error handling is an essential aspect of server programming. There are different approaches to working with errors. It will demonstrate an approach using a custom constructor for instances of the Error object and error codes that are conveniently passed to the frontend or to any mechanisms using server APIs. How the backend of a specific project is structured doesn’t matter much, as with any approach you can use the same ideas for dealing with errors.

As a server framework responsible for routing, we will use Express.js. Let's think about what structure we need to organize an effective error handling system. So, this is what we need:

  1. Universal error handling is a kind of basic mechanism suitable for handling any errors, during which a message like Something went wrong, please try again or contact us , prompting the user to try the failed operation again or contact the owner of the server. This system is not particularly intelligent, but at least it is able to inform the user that something has gone wrong. This message is much better than “endless loading” or something similar.
  2. The handling of specific errors is a mechanism for informing the user of the details of the reasons for the incorrect behavior of the system and giving it specific advice on how to deal with the problem. For example, this may concern the absence of some important data in the request that the user sends to the server, or that there is already some record in the database that he is trying to add again, and so on.

â–ŤDeveloping your own error object constructor


Here we will use the standard Error class and expand it. Using inheritance mechanisms in JavaScript is risky, but in this case, these mechanisms are very useful. Why do we need inheritance? The fact is that in order for us to make the code convenient to debug, we need information about the error stack trace. Expanding the standard Error class, we, without additional efforts, get the opportunity to trace the stack. We add two properties to our own error object. The first is the code property, which can be accessed using an err.code . The second is the status property. The HTTP status code will be written to it, which is planned to be transferred to the client part of the application.

This is how the CustomError class looks, the code of which is designed as a module.

 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 

â–ŤRouting


Now that our error object is ready for use, we need to set up the structure of the routes. As mentioned above, we need to implement a unified approach to error handling, which allows us to handle errors for all routes in the same way. By default, the Express.js framework does not fully support this workflow. The fact is that all its routes are encapsulated.

In order to deal with this problem, we can implement our own route handler and define the logic of the routes as normal functions. Thanks to this approach, if the route function (or any other function) throws an error, it will end up in the route handler, which can then be passed to the client-side of the application. If an error occurs on the server, we plan to send it to the frontend in the following format, assuming that JSON-API will be used for this:

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

If at this stage what is happening seems incomprehensible to you - do not worry - just keep reading, try to work with what is at stake, and gradually you will understand everything. In fact, if we talk about computer training, the “top-down” approach is used here, when general ideas are discussed first, and then the transition to particulars takes place.

Here is the route handler code.

 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 

We believe that the comments in the code explain it quite well. We hope it is more convenient to read them than explanations of the similar code given after it.

Now take a look at the route file.

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

In these examples nothing is done with the queries themselves. It just looks at different error scenarios. So, for example, the GET /city request gets into the function const GET = req =>... , the POST /city request gets into the function const POST = req =>... and so on. This scheme also works when using query parameters. For example - for a request of the form GET /city?startsWith=R In general, it is demonstrated here that when handling errors, the frontend can be either a general error, containing only a suggestion to try again or contact the server owner, or an error generated using the CustomError constructor, which contains detailed information about the problem.
General error data will come to the client part of the application in the following form:

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

The CustomError constructor is used like this:

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

This gives the following JSON code transmitted to the frontend:

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

Now that we have thoroughly worked on the server part of the application, the useless error logs no longer fall into the client side. Instead, the client gets useful information about what went wrong.

Do not forget that there is a repository with the code considered here. You can download it, experiment with it, and, if necessary, adapt it to the needs of your project.

3. Work with errors on the client


Now it's time to describe the third part of our error handling system, concerning the frontend. Here it will be necessary, firstly, to handle errors that occur in the client part of the application, and secondly, you will need to notify the user about errors that occur on the server. We will first understand the display of information about server errors. As already mentioned, this example will use the React library.

â–ŤSaving error information in the application state


Like any other data, errors and error messages can change, so it makes sense to put them in the state of the components. When a component is mounted, the error data is reset, so when the user first sees the page, there will be no error messages.

The next thing you need to understand is that errors of one type should be shown in the same style. By analogy with the server, here you can select 3 types of errors.

  1. — , , , , , , .
  2. , — , . , , , , . , .
  3. , . — , .

, , . , . , , .

React , , , — , MobX Redux.

â–Ť


, . . , . . .




, 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 ), , . , , , , , , , , . .

Results


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

Dear readers! ?

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


All Articles