Insights on Error Handling System Design

Importance of Error-Handling System

During the early phases of developing FoxiApply, we lacked a comprehensive error-handling system. Every time we encountered a bug, developers had to add console.log(*) to the entire codebase to search for the root of an error, which resulted in low efficiency. Later on, we started to realize the necessity of the implementation of a robust and solid error-handling system. An effective system can

  1. Facilitate smooth application functionality, reducing major service interruptions.
  2. Improve the efficiency of development, and help the developer swiftly pinpointing error sources.
  3. Streamline code logic, ensuring it remains neat and organized.

Embrace Custom Error Classes

The first step is to define our own Error Classes. This allows for tailored error-handling strategies for distinct error categories. Common web application error types include:

  • ApiError: external API errors
  • DataBaseError: database-related errors
  • AuthError: authentication-related errors
  • BadRequestError: user illegal requests
  • DataError: internal logic errors
  • So on …

Below is a sample JavaScript custom error class definition:

class CustomError extends Error {
    constructor(message, errorType, statusCode, property1,..., propertyN) {
        super(message);
        this.name = this.constructor.name;
        this.errorType = errorType;
        this.statusCode = statusCode;
        this.property1 = property1;
        ...
        this.propertyN = propertyN;

        // Set the prototype explicitly.
        Object.setPrototypeOf(this, CustomError.prototype);
    }

    otherFunction(){
      ...
    }
}

Employ Try-Catch Judiciously

One oversight during my error handling system’s initial stages was the excessive use of the try-catch structure. Instead, the ideal approach is to:

  1. Wrap the error at the endpoint, converting it to a recognizable form.
  2. Introduce a try-catch structure at the root level, and use the subsequent function to pass the error to a global handler.

Note that during error wrapping, the original error remains untouched, being stored in a property (e.g., originalError). This prevents loss of the initial error message.

router.get('./route', async (req, res, next) => {
    try{
        await func_1(...args);
    } catch (error) {
        next(error);
    };
});

async func_1(...args){
    return await func_2(...args)
}

...

async func_i(...args){
    return await func_j(...args)
}

...

async func_n(...args){
    try{
        callAPI(...args)
    }catch(err){
      throw new errorWrapper(err)
    }
}

Use a Global Error Handler

Imagine the global error handler as a traffic control center, processing errors in the same way it manages incoming buses and trains. Just as the traffic center directs various vehicles along specific routes, the error handler applies tailored strategies to different error types. Such a centralized error handler ensures clarity in code logic and facilitates straightforward error logging.

Apply Different Strategies for Different Errors

One of the biggest problems in error-handling system design is how to deal with different types of errors. Since we have defined our own custom error types and global error-handler middleware before, now it is easy to come up with a Switch structure like the following to solve the problem. For authentication errors, it needs to redirect the user to the login page; for bad request errors, it needs to show the error message to the user to notify them that they made illegal operations. This can be achieved by setting different properties to the Custom Error Object, like error.redirectURL indicates the redirect URL address and error.showMessage indicates whether we need to show it to the user.


const errorHandler = (err, req, res, next) => {

    logger.error(...)

    if(err instanceof CustomError){

        if(err instanceof AuthError){
            ...
        }

        else if (err instanceof DataError){
            ...
        }

        ...
        
        return res.status(err.statusCode || 500).send(...)
    }

    else{
        res.status(500).send(...)
    }

}

Special Case: Front-End Errors

There are actually exceptions during the entire error-handling system since there are also errors whose entire lifecycle is within the frontend. For example, we have an upload button that only accepts pdf files, if the user uploads any files of other formats, it will refuse it, trigger a request error, and show the error on the screen. This kind of error would not make any request call to the server so it happens with the frontend.

Conclusion

These insights arose from my journey in creating an error-handling system for FoxiApply. Reflecting on this process, the adage “A stitch in time saves nine” seems apt. Investing in a reliable error-handling system now can reap significant dividends down the road.

Chengzhan Gao
Chengzhan Gao
高程展|Graduate Student at UCSD

My interests include web development, game development and machine learning.