Build Custom Error Classes for error-handling in JS and TS

Pre-requisites: try-catch block, Classes & Inheritance in JavaScript

BUT... Why do we need Custom Error Classes?

This is important to know, otherwise, why do anything :)

  • Clearer and more descriptive errors: Custom error classes allow you to define specific error types by providing meaningful names and messages for each error type, you can make error handling and debugging easier for developers.

    For Example:

    • If you are handling bad inputs or requests from the client side, you can create an Error class called BadRequestError.

    • You can run into different types of errors while working with authentication like wrong password, invalid email id, and expired token.

      For such cases, we can create a custom class like AuthError that handles and defines all various types of errors regarding authentication and authorization.

    • Using another class like DatabaseConnectionError when your server fails to connect to the database.

  • Improved code readability: Custom error classes make your code more expressive and self-documenting. When developers encounter custom error classes in the codebase, they can quickly understand the specific error scenarios and the corresponding handling logic, leading to improved code readability and maintainability.

  • Custom error-specific behaviors: By extending the base Error class or creating custom error classes, you can define additional methods or properties specific to certain error types. This allows you to add custom behavior or encapsulate error-specific logic within the error class itself, making it easier to handle and respond to different error scenarios.

    For example:
    Adding properties like timeStamp when the error occurred, error codes, API Endpoints, and the description of the error can give more information about an error that has occurred. It enables better error reporting, logging, and troubleshooting.

  • Consistent error handling: With custom error classes, you can establish a consistent error handling pattern across your application. By throwing and catching specific error types, you can handle different types of errors in a unified manner and apply appropriate error-handling logic.

    For Example:

    • All error instances of AuthError class that are used in the auth module or code will have the same properties like auth_error_code, field (that stores 'email' or 'password'), reason, and methods like serializeErrors() that will return an array of error messages.

    • This way, every error triggered in the auth module, can return errors to the client side while maintaining its structure.

  • Integration with existing error handling mechanisms: Custom error classes can seamlessly integrate with existing error handling mechanisms in your chosen programming language or framework. They can be caught and handled using standard error-handling constructs like try...catch blocks, making it straightforward to incorporate them into your error-handling workflow.


Built-in Error Class?

The built-in Error class in JavaScript is used to represent errors that occur during the execution of a JavaScript program. It provides several properties that can be used to provide information about the error, such as the message, the name of the error, and the stack trace.

The Error class can be used to throw exceptions, which are objects that represent errors that can be caught and handled by other parts of the program. To throw an exception, you can use the throw keyword, followed by an instance of the Error class. For example:

Code without try-catch block

const error = new Error("This is an error");
throw error;

/** error object
{
    name: '...',
    message: '...'
}
*/

When you throw an object, JavaScript will look for a catch block that can handle the thrown object based on its type. If a catch block is not found, the exception will propagate up the call stack until it is caught or until it reaches the global scope, where it can cause the program to terminate.

In the above code error is an object of the Error class and a string is passed to Error() Constructor that will set a custom message to the message property of the error object.

Code with try-catch block

function divide(a, b) {
  if (b === 0) {
    // this error thrown is caught in the catch block
    throw new Error("Divisor cannot be zero");
  }
  return a / b;
}

try {
  const result = divide(10, 0);
  console.log(result);
} catch (error) {
  // error is an instance of the Error class
  console.log(error.message); // Error message: Divisor cannot be zero
  console.log(error.name); // Error name: Error
  console.log(error.stack); // Stack trace
}

If an exception is thrown from throw new Error("Divisor cannot be zero"), the JavaScript engine will search for a catch statement that matches the type of the exception. Since a catch statement is found, the execution of the program will continue with the code inside the catch statement.

The throw new Error("Divisor cannot be zero") line basically throws
an object of a class.

This is the object: new Error("Divisor cannot be zero")

To know more about try-catch, follow this article


Custom Error Classes

Now the good news is we can build our Error Classes with their own properties and methods based on the requirement of the project and the code written.

Let's understand Custom Error Classes with some code snippets

Code Snippet #1

// custom error called AppError
class AppError extends Error {
    constructor(message){
        super(); // important to call whenever inheriting a parent class
        this.message = message;
    }
}

try{
    throw new AppError('Testing Custom Error Class'); 
}
catch(err){
    console.log(err);
}

Output:

AppError: Testing Custom Error Class
    at Object.<anonymous> (/path/to/your/code/file.js:9:11)
    ...

Let's understand the code:

  • I have created a custom error class called AppError that extends the Error Class. It inherits properties like name, message, stack and methods like toString() of Error Class.

  • super() invokes the constructor of the Error class to initialize the instance variables if any.

  • A custom message 'Testing Custom Error Class' is set to this.message, where the message is the property of AppError.

  • throw new AppError("Testing Custom Error Class") basically means:

    • throw an error object, where this error object is an instance of AppError that contains both the properties of AppError and Error classes.

    • So this error object, say err which is caught in the catch block has access to props like name, message and stack and toString() method.

Code Snippet #2

// custom error called AppError
class AppError {
    constructor(message){
        this.message = message;
    }
}

try{
    throw new AppError('Testing Custom Error Class'); 
}
catch(err){
    console.log(err);
}

Output:

AppError { message: 'Testing Custom Error Class' }

Why is it different from Code Snippet #1?

  • As you can see, here the AppError does not extend the Error Class. But when throw new AppError() is executed, the try block throws an error and is caught in the catch block.
    But here, we can only see the message property of the AppError Class.

  • The throw statement generates a user-defined exception and throws the error object out of the current block (any statement after throw won’t be executed), and control will be passed to the first catch block.

  • But as you can see in the output, the error object does not have properties like name and stack.

  • Inheriting the Error class gives you that benefit! As the error stack provides the path to the root cause of the error which is important for debugging purposes.


Another Example of Using Custom Error Class

const statusCodes = require('../config/statusCode');

/**
 * @param {number} status - The HTTP status code
 * @param {string} message - Contains custom error messages (if provided) or default error messages
 * @param {string} apiEndpoint - Contains the api endpoint where the error has occurred
 * @methods toString( ), toJSON( )
 * @properties status, apiEndpoint, timestamp, name, description
 * @description  Class create Error instances for Auth related errors or unwanted attempts.
 */


class AppError extends Error {
    constructor(status, message = '', apiEndpoint = 'Unknown') {

        // invokes the constructor of the Error Class
        super();

        // setting the status code 400, 401, 500, 505, etc.
        this.status = status;

        // setting message and description using status
        this.message = message;
        this.description = "A detailed Description for this error";

        // additional details for finding errors

        // time when error is triggered
        this.timestamp = new Date().toISOString();

        // API Endpoint where the error occured
        this._apiEndpoint = apiEndpoint;
    }

    /* 
    Override the default toString method to provide a better error message
    the message provides: timestamp, error name, statusCode, and error message
    */
    toString() {
        return `${this.apiEndpoint} - ${this.name} [${this.status}]: ${this.message}`;
    }

    // toJSON method to return a JSON representation of the error
    toJSON() {
        return {
            name: this.name,
            status: this.status,
            message: this.message,
            timestamp: this.timestamp,
            apiEndpoint: this._apiEndpoint,
            description: this.description
        }
    }
}

module.exports = AppError;

If you look carefully, I have created a custom error class that has its own properties and methods.

We can always design our own classes and for more benefits like error stack, we need to extend the Error Class.

Here the toString() method of AppError overrides the toString() method of the Error Class.

Read the comments provided in the above code snippet for better understanding.

Now I can create another custom error class that extends the properties and methods of AppError

const AppError = require("./AppError");

class AuthError extends AppError {
    constructor(status, message, apiEndpoint) {
        super(status, message, apiEndpoint);
        this.name = this.constructor.name;
    }
}

module.exports = AuthError;
  • The code defines a new class called AuthError that extends the AppError class.

  • The AppError class is imported using the require function.

  • The AuthError class has a constructor that takes three arguments: status, message, and apiEndpoint.

  • Inside the constructor, the super function is called with the arguments status, message, and apiEndpoint. This calls the constructor of the parent class (AppError) and passes these arguments to it.

  • The name property of the instance is set to the name of the constructor, which is "AuthError".

  • The AuthError class is exported using the module.exports statement.


Custom Classes in TS

Let's see how we can create an Error Class in TypeScript.

class BadRequestError extends Error {
    statusCode: number = 400;

    constructor(public message: string) {
        super(message);

        Object.setPrototypeOf(this, BadRequestError.prototype);
    }

    serializeErrors() {
        return { message: this.message }
    }
}

const err = new BadRequestError('This is a test');
console.log(err.serializeErrors());

Let's Understand what's going on:

  • statusCode and message are instance variables of BadRequestError class.

  • serializeErrors() is a method to return an object that contains the error message

But what about this? Object.setPrototypeOf(this, BadRequestError.prototype)

  • this is the instance of the Class created.

  • BadeRequestError.prototype is the prototype of the class BadRequestError that defines all the properties and methods of the same class. It is an object property of the class.

  • Object.setPrototypeOf(this, BadRequestError.prototype): When you create a new instance of a class, the new instance inherits all the properties and methods from the class's prototype using Object.setPrototypeOf()

This is done to ensure that the instance created has access to the methods and properties which are defined on the BadRequestError.prototype, such as the serializeErrors(), statusCode and message.

If I run the above code, it will return:

{ message: 'This is a test' }

But what if I disable this line Object.setPrototypeOf(this, BadRequestError.prototype)

class BadRequestError extends Error {
    statusCode: number = 400;

    constructor(public message: string) {
        super(message);

        // Object.setPrototypeOf(this, BadRequestError.prototype);
    }

    serializeErrors() {
        return { message: this.message }
    }
}

const err = new BadRequestError('This is a test');
console.log(err.serializeErrors());

If I run this code, it will throw an error:

TypeError: err.serializeErrors is not a function

The method serializeErrors() is undefined on err because the err object is not set with the prototype of the class, that's why it does not have access to the method.

That's why using Object.setPrototypeOf() is important . . .


The End... :)

Comment about the next concept or topic you would like to know to enhance your skills in backend development.

Follow for more such content.

Like if you found this article useful :)

Feedback would be appreciated.

Share if possible :D

Peace ✌️

Did you find this article valuable?

Support Mayukh Bhowmick by becoming a sponsor. Any amount is appreciated!