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 liketimeStamp
when the error occurred, errorcodes
, APIEndpoints
, and thedescription
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 likeserializeErrors()
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 theError
Class. It inherits properties likename
,message
,stack
and methods liketoString()
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 tothis.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 likename
,message
andstack
andtoString()
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 theError
Class. But whenthrow 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 theAppError
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 firstcatch
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 theAppError
class.The
AppError
class is imported using therequire
function.The
AuthError
class has a constructor that takes three arguments:status
,message
, andapiEndpoint
.Inside the constructor, the
super
function is called with the argumentsstatus
,message
, andapiEndpoint
. 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 themodule.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
andmessage
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 classBadRequestError
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 usingObject.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