nodejs-exception and global exception handling

This article can be regarded as a phased summary of abnormalities based on the knowledge I have learned. I cannot guarantee that there is no problem with my understanding. If there is something wrong, please point it out.

To read this article, you need to understand promise, async await, nodejs, koa, js asynchronous, etc. These additional knowledge are not explained in this article

Article directory

    • Anomaly Theory and Exception Chaining
    • Asynchronous exception handling scheme
      • Asynchronous exception case
      • Asynchronous exception handling scheme
    • Implementation of global exception handling middleware
      • Basic implementation
      • Known errors and unknown errors
      • Define the exception return format
      • Define exception base class
      • Define specific exception classes
      • mount global

Exception theory and exception chain

  1. The method of exception handling is believed to be familiar to everyone. Grammar is just a trycatch, but in programming, grammar is only a means to solve problems. How to use this method to solve problems better and more reasonably requires some thought. to design

  2. In the process of function execution, there are generally two situations, the first is that no exception occurs, and the second is that an exception occurs

  3. Usually when we use functions to write code, we will do some processing for some functions that may feel that errors may occur, such as returning false or undefined to make some exception processing, but if we follow a programming specification, a For errors, we should use throw to throw an exception, and then use trycatch to catch it where the exception will be thrown, such as the following code:

    function func1() {<!-- -->
    try {<!-- -->
    func2()
    } catch (error) {<!-- -->
    console.log(error) // func2 error
    }
    }
    
    function func2() {<!-- -->
    throw 'func2 error'
    }
    
    func1()
    
  4. Then let’s talk about why it is not recommended to return a false or null instead of a throw to throw an exception, because in many cases we need to use an exception information to debug a code, if there is no information about the exception , it will bring a lot of burden to our debugging. Let’s take a look at the following case, as follows:

    function func1() {<!-- -->
    try {<!-- -->
    func2()
    } catch (error) {<!-- -->
    console. log(error)
    console.log('---------------')
    }
    }
    
    function func2() {<!-- -->
    try {<!-- -->
    func3()
    } catch (error) {<!-- -->
    throw error
    }
    }
    
    function func3() {<!-- -->
    a
    }
    func1()
    
  5. a is an attribute that is used without definition. In terms of code execution, an error will definitely be reported. If we simply return a false at this time, it will be difficult for us to clear what error occurred in func1. Let’s take a look at the result of using throw, as shown in the figure:

  6. An error message like this can clearly point out the error and reduce our debugging costs, but this goes back to the original problem. This method can be solved, but it is not good enough. Every function of this kind needs to write a trycatch to capture Exceptions can certainly solve our needs. If the number of calls between functions increases, it is conceivable that each function has to write a trycatch. The degree of trouble is conceivable. In actual coding, we even find it troublesome to repeat an assignment statement, so what? Can you write code that catches exceptions like this?

  7. It is also possible to say that I only need to capture the places where exceptions may occur, not to mention whether the places where we think there will be no exceptions will definitely not appear, only that when we use some third-party libraries, We can’t control these codes like we control the code we write, so a process that can globally catch exceptions is particularly important

  8. What is global capture? Just like our above case, we don’t need to capture the exception thrown by the func3 function in func1 and func2. We only need to use this global exception handling to capture it, which can reduce our development costs. and improve code security

  9. Then why do you need this exception? I believe that when you are doing web development, you will often fall in love with some status codes, such as the common 404, 403, etc., but the errors defined by these common status codes are relatively broad, and many times they are Some detailed errors cannot be expressed. At this time, we need to obtain these exceptions to define some error messages and error codes designed by ourselves. This error code is very necessary, and it can make our development smoother. , can also reduce the communication cost of front-end and back-end, and when we use some third-party libraries, only by providing such detailed errors to users can users know what kind of errors

Asynchronous exception handling scheme

Asynchronous exception case

  1. Above we have given an example of exception handling, but the above method cannot catch asynchronous exceptions, we can test it, as follows:

    function func1() {<!-- -->
    try {<!-- -->
    func2()
    } catch (error) {<!-- -->
    console.log('---------------')
    console. log(error)
    console.log('---------------')
    }
    }
    
    function func2() {<!-- -->
    setTimeout(() => {<!-- -->
    a
    }, 1000)
    }
    
    func1()
    
  2. If it is handled according to the exception capture, we should print the exception error in func1 after the exception occurs, and the result is as shown in the figure:

  3. Of course the exception was thrown, but it was not thrown where we expected, why? This is actually easy to understand. setTimeout will add the executed task to the macro task queue. The macro task is an asynchronous queue. The execution of the asynchronous queue needs to wait until the synchronous task is executed. When func2 is executed, the setTimeout callback None of them are executed, func2 returns an undefind, so it is normal that it cannot be caught

Asynchronous exception handling scheme

  1. How can we handle asynchronous exceptions less? Our old buddies promise and async await, here we don’t use callbacks to handle them. It is very troublesome to handle them, at least compared to promises, it is much more troublesome in terms of writing

  2. I won’t introduce promise here, let’s take a look at using promise to handle the above exception throwing, as follows:

    async function func1() {<!-- -->
    try {<!-- -->
    await func2()
    } catch (error) {<!-- -->
    console.log('---------------')
    console. log(error)
    console.log('---------------')
    }
    }
    
    async function func2() {<!-- -->
    return new Promise((resolve, reject) => {<!-- -->
    setTimeout(() => {<!-- -->
    reject('func2 a fault has occurred')
    }, 1000)
    })
    }
    
    func1()
    
  3. The result is shown in the figure:

Implementation of global exception handling middleware

Basic implementation

  1. Here I am using the koa framework of nodejs to demonstrate. In koa, the middleware is a function. The following is a basic koa code, as follows:

    const Koa = require('koa')
    const Router = require('koa-router')
    const bodyparser = require('koa-bodyparser')
    
    const app = new Koa()
    const router = new Router({<!-- --> prefix: '/api' })
    
    app. use(bodyparser())
    
    router.get('/data', (ctx, next) => {<!-- -->
    ctx.body = 'hello world'
    })
    
    app. use(router. routes())
    app.use(router.allowedMethods())
    
    app.listen(6346, () => {<!-- -->
    console.log('service start success~')
    })
    
  2. As for the meaning of these, I will not explain here. The source code will provide a link to download at the end. In koa, a middleware is actually a function, and can accept two parameters, one ctx and one next. Since it is exception handling, then we It is inevitable to use trycatch, and the middleware in koa is promise by default, so we can match async and await, so we can get a basic function signature, as follows:

    const handleError = async (ctx, next) => {<!-- -->
    try {<!-- -->
    // no error continue to next step
    await next()
      } catch (error) {<!-- -->
        // return a basic error
            ctx.body = 'An error occurred inside the server, please wait'
      }
    }
    
  3. Now let’s use this middleware as follows:

    const Koa = require('koa')
    const Router = require('koa-router')
    const bodyparser = require('koa-bodyparser')
    
    const app = new Koa()
    const router = new Router({<!-- --> prefix: '/api' })
    
    app. use(bodyparser())
    
    // exception handling
    const handleError = async (ctx, next) => {<!-- -->
    try {<!-- -->
    // no error continue to next step
    await next()
    } catch (error) {<!-- -->
    ctx.body = 'An error occurred inside the server, please wait'
    }
    }
    
    // can also be written before routing execution
    // app. use(handleError)
    
    router.get('/data', handleError, (ctx, next) => {<!-- -->
    throw 'error'
    })
    
    app. use(router. routes())
    app.use(router.allowedMethods())
    
    app.listen(6346, () => {<!-- -->
    console.log('service start success~')
    })
    
  4. Here I define the get request, and I use the browser to send the request for testing. Of course, I can also use postman and other testing tools. The test results are shown in the figure:

  5. We can clearly define an error like this, which is obviously much better than a broad error definition, and with this global exception handling middleware, the increase in the function call chain will not increase for us additional development burden

Known errors and unknown errors

  1. Normally, an error message, in addition to the information of the error itself, also has the call information of the stack, as shown in the figure:

  2. And these errors should not all be returned to the front end, so we should simplify and standardize these error messages, which are very common in web development

  3. Generally, it will return to the front-end HTTP status code (status code), complete status code description, error message with text description, and custom error code. Generally, these three error fields will be returned. Of course, according to the specifications and business requirements Different, the fields carried will also be different, for example, we can carry an additional request path, this field contains the request method of this wrong request, that is, get, post… and the path of this request

  4. What is a known error, such as the parameter type does not meet the requirements, the parameter does not match, this kind of error that can be judged is a known error, and the potential error of the unknown error program is unknown to the developer, such as the link If the account password of the data is wrong, it is unknown. If it is known, it must be replaced with the correct account password.

  5. We can throw known errors correctly, and unknown errors will also be caught by global exception handling, so this is also where we need to distinguish between known and unknown. We need to judge whether the error is a known type or Unknown type for different error handling

Define exception return format

  1. We have already talked about the format of returning error types: HTTP status code, message, errorCode, requestUrl, which we can wrap into an object

  2. The format is as follows:

    ctx. body = {<!-- -->
        msg: error message,
        errorCdoe: xxxxx,
        requestUrl: 'POST /api/data'
    }
    // In koa, specify the HTTP status code of the returned request result this time, which can be realized through ctx.status
    ctx.status = status code
    

Define exception base class

  1. After defining the returned error format, we can now define the exception class that needs to be returned. This method of class has strong encapsulation, so it is more appropriate to use it here

  2. To realize this idea, we can let the exception base class inherit from the error class Error. I will not talk about the design of the project here, but just create a separate file (error-tyoe.js) to implement the error base class

  3. We can write the following constructor according to the parameters we need to pass, as follows:

    class ErrorTypeModule extends Error {<!-- -->
    constructor(status, errorCode, msg) {<!-- -->
    // Use the constructor of the parent class
    super()
    \t\t// status code
    this.statusCode = status
    \t\t// error code
    this.errorCode = errorCode
    // error message
    this.msg = msg
    }
    }
    
    module.exports = {<!-- -->
    ErrorTypeModule
    }
    
  4. There may be two doubts here, requestUrl is not defined, this will be known later, mainly why we need to inherit the Error class of nodejs, when we write the business interface, we need to throw an error when we find an error, and use throw When an error is thrown, it needs to be an Error type of data, so it needs to inherit from the Error class

  5. Next, we will actually introduce these into the code and test it, as follows:

    const Koa = require('koa')
    const Router = require('koa-router')
    const bodyparser = require('koa-bodyparser')
    // import exception base class
    const {<!-- --> ErrorTypeModule } = require('./error-type')
    
    const app = new Koa()
    const router = new Router({<!-- --> prefix: '/api' })
    
    app. use(bodyparser())
    
    // exception handling
    const handleError = async (ctx, next) => {<!-- -->
    try {<!-- -->
    // no error continue to next step
    await next()
    } catch (error) {<!-- -->
    // Here you can judge whether it is an exception base class, if it is, it means a known type of error
    if (error instanceof ErrorTypeModule) {<!-- -->
    // The wrong path can be added here
    error.requestUrl = `${<!-- -->ctx.request.method} ${<!-- -->ctx.request.url}`
    // set http status code
    ctx.status = error.statusCode
                // return the entire error message
    ctx.body = error
    }
    }
    }
    
    // can also be written before routing execution
    // app. use(handleError)
    
    router.get('/data', handleError, (ctx, next) => {<!-- -->
    // Replace the way of throwing an error
    // Inherit Error
    // - Why do we have to inherit the Error class of nodejs
    // - because we need to throw this custom error message
    // - and the throw needs to be an Error type
    const error = new ErrorTypeModule(400, 10001, 'An error occurred inside the server')
    throw error
    })
    
    app. use(router. routes())
    app.use(router.allowedMethods())
    
    app.listen(6346, () => {<!-- -->
    console.log('service start success~')
    })
    
  6. Take a look at the output results, as shown in the figure:

Define specific exception classes

  1. Now we still have a problem when we throw an error, we need to pass in three parameter values, and in actual business development, the same error will inevitably occur, so we can choose to define each error type as A specific class, as follows:

    class ErrorTypeModule extends Error {<!-- -->
    constructor(status, errorCode, msg) {<!-- -->
    super()
    this.statusCode = status
    this.errorCode = errorCode
    this.msg = msg
    }
    }
    
    // ----------------- Define specific exception class -----------------
    
    // parameter type error
    class ParamsError extends ErrorTypeModule {<!-- -->
    // For some specific exception classes that we define directly, we can set some default parameters
    // - Allows us to use without passing parameters if there is no special information
    constructor(errorCode, msg) {<!-- -->
    super()
    // The http status code of the wrong parameter type is relatively fixed as 400, in this case, the fixed value can be used directly
    this.statusCode = 400
    this.errorCode = errorCode || 10000
    this.msg = msg || 'parameter error'
    }
    }
    
    module.exports = {<!-- -->
    ErrorTypeModule,
    ParamsError
    }
    
  2. Put it into the code test, as follows:

    const Koa = require('koa')
    const Router = require('koa-router')
    const bodyparser = require('koa-bodyparser')
    // import exception base class
    const {<!-- --> ErrorTypeModule, ParamsError } = require('./error-type')
    
    const app = new Koa()
    const router = new Router({<!-- --> prefix: '/api' })
    
    app. use(bodyparser())
    
    // exception handling
    const handleError = async (ctx, next) => {<!-- -->
    try {<!-- -->
    await next()
    } catch (error) {<!-- -->
    if (error instanceof ErrorTypeModule) {<!-- -->
    error.requestUrl = `${<!-- -->ctx.request.method} ${<!-- -->ctx.request.url}`
    ctx.status = error.statusCode
    ctx.body = error
    }
    }
    }
    
    router.get('/data', handleError, (ctx, next) => {<!-- -->
    // When using it, we can directly throw this specific exception class
    throw new ParamsError()
    })
    
    app. use(router. routes())
    app.use(router.allowedMethods())
    
    app.listen(6346, () => {<!-- -->
    console.log('service start success~')
    })
    
  3. The output result is shown in the figure:

Mount global

  1. After encapsulating the exception class, it is inevitable to import this file separately in each interface file, which may also be troublesome, so you can mount these exception classes to the global variable global, which is a global variable provided by nodejs

  2. The modified code is as follows:

    const Koa = require('koa')
    const Router = require('koa-router')
    const bodyparser = require('koa-bodyparser')
    // import exception base class
    // const { ErrorTypeModule, ParamsError } = require('./error-type')
    // Hang the exception class on the global when the entry file starts
    const error = require('./error-type')
    global.errors = error
    
    const app = new Koa()
    const router = new Router({<!-- --> prefix: '/api' })
    
    app. use(bodyparser())
    
    // exception handling
    const handleError = async (ctx, next) => {<!-- -->
    try {<!-- -->
    await next()
    } catch (error) {<!-- -->
    // if (error instanceof ErrorTypeModule) {<!-- -->
    if (error instanceof global.errors.ErrorTypeModule) {<!-- -->
    error.requestUrl = `${<!-- -->ctx.request.method} ${<!-- -->ctx.request.url}`
    ctx.status = error.statusCode
    ctx.body = error
    }
    }
    }
    
    router.get('/data', handleError, (ctx, next) => {<!-- -->
    // throw new ParamsError()
    // use exception class on global variable
    throw new global.errors.ParamsError()
    })
    
    app. use(router. routes())
    app.use(router.allowedMethods())
    
    app.listen(6346, () => {<!-- -->
    console.log('service start success~')
    })
    
  3. For testing, you can test it yourself, and I won’t show it here. The specific method of importing depends on your own habits.