Custom error classes in Javascript

Anton Ioffe - September 13th 2023 - 12 minutes read

Have you ever been left scratching your head while debugging in JavaScript only to find that the error message is incredibly vague or, even worse, nonexistent? You're not alone! Many developers wish JavaScript had more specific error messages and that's where custom error classes come in. In this article, we will dive deep into the realms of error handling and management in JavaScript, focusing primarily on custom error classes.

We will start with the fundamental concepts, then transition into creating and using custom error classes. By using illustrative and annotated code examples, we aim to simplify complex concepts that often baffle even seasoned developers. From there, we will discuss the importance of proper error management, how-to expertly 'throw', 'catch', and finally 'manage' any errors thrown your way.

And it doesn't stop at just the basics. We will explore the practical benefits of custom error classes by shedding light on real-life applications, and showcasing how such techniques can improve code readability, reusability, and overall performance. Finally, we'll round up by explaining how these concepts apply to a Node.js environment - an important focus for full-stack developers today. So buckle up, and let's step into the fascinating world of custom error classes in JavaScript!

Basics of Error Handling in JavaScript

To dive into error handling, it's important to know that JavaScript, like many other coding languages, comes with various built-in error types. These error types help developers to spot and fix problems in their code. These issues can come from runtime challenges, syntax mistakes, or logic errors that lead to unexpected code behavior.

JavaScript Error Types

JavaScript comes with seven built-in error types:

  1. The Error forms the base for all other error types.
  2. EvalError crops up when there's an issue within the eval() function.
  3. RangeError appears when a number goes beyond an acceptable range.
  4. ReferenceError shows up when there’s an attempt to use a variable that doesn't exist.
  5. SyntaxError occurs when there's a syntax error during parsing of JavaScript code.
  6. TypeError emerges when a parameter or operand isn't of the expected type.
  7. URIError happens when the encodeURI() or decodeURI() functions are misused.

Remember, these errors are object instances, and we throw them with the throw keyword. Let's illustrate this with an example where a TypeError instance is thrown:

function checkNumber(num) {
    if (typeof num !== 'number') {
        throw new TypeError('checkNumber expects only number');
    }
}

try {
    checkNumber('hello'); // This will trigger a TypeError
} catch (error) {
    console.error(error.message); // Output: checkNumber expects only number
}

Common Misstep: Not Throwing Error Instances

Too often, developers mistakenly throw errors as strings. This is a common slip-up. A best practice is to always throw instances of Error or its subclasses. It gives a great debugging context, such as the stack trace.

Incorrect Way:

throw 'This is an error'; // Not correct

Correct Way:

throw new Error('This is an error'); // Correct way

Understanding the Error Object

Handling errors in JavaScript is not just about the throw keyword. The Error object is equally important.

The Error object is the base object for all error types. It offers two main properties:

  1. message: A human-readable description of the error.
  2. name: A specific name of the error (usually referring to the error type).

Let's run through an example of creating an Error object:

const errorInstance = new Error('This is an error instance');

console.log(errorInstance.name); // Output: Error
console.log(errorInstance.message); // Output: This is an error instance

By understanding the basics of error handling, you've opened up many possibilities for leveraging JavaScript's flexibility and object-oriented power. So, ask yourself, "How could I optimize or upgrade my code by getting a good grasp of these basic error handling techniques?"

Creating Custom Error Classes in JavaScript

To create custom error classes in JavaScript, it's essential to have a good understanding of the class and extends keywords, as well as the constructor function. This allows us to exploit JavaScript's prototype-based inheritance system, coupled with its class-like syntax, to manage custom errors more effectively.

Passing Error Messages to Custom Error Classes

Consider a case where you need to handle a specific type of error, such as an http error. Typically, you might consider using the Error class to throw an error but with custom error classes, we can make the error type more specific. For this, we'll use the class and extends keywords. Here's a simple example:

class HttpError extends Error {
    constructor(message) {
        super(message); // Calls the parent constructor
        this.name = 'HttpError';
    }
}

try {
    throw new HttpError('404 Not Found');
} catch (err) {
    console.log(err.name); // 'HttpError'
    console.log(err.message); // '404 Not Found'
}

In this example, we've created a custom HttpError class that extends JavaScript's built-in Error class. In the constructor, we call super(message) to access the parent Error class's constructor and set the error message. We then overwrite the error name with 'HttpError'.

Common Mistake: Not extending the built-in Error Class

One common mistake when creating custom error classes in JavaScript is forgetting to extend the built-in Error class. The downfall of this mistake is that the created class does not inherit the properties of the Error class.

Below is an example of this mistake:

class CustomError {
    constructor(message) {
        this.message = message;
        this.name = 'CustomError';
    }
}

try {
    throw new CustomError('Something went wrong');
} catch (err) {
    console.log(err.stack); // undefined
}

As you can see, the stack property of the error object is undefined because CustomError doesn’t inherit the properties and methods of the Error class. The corrected version, with extends Error, would look like this:

class CustomError extends Error {
    constructor(message) {
        super(message);
        this.name = 'CustomError';
    }
}

try {
    throw new CustomError('Something went wrong');
} catch (err) {
    console.log(err.stack); // Outputs error stack trace
}

Handling Specific Error Types

Another useful feature of custom error classes is the ability to handle specific types of errors. Expanding on the HttpError example, we could create a new NotFoundError class that extends HttpError instead of Error directly.

class NotFoundError extends HttpError {
    constructor(message) {
        super(message);
        this.name = 'NotFoundError';
        this.statusCode = 404;
    }
}

try {
    throw new NotFoundError('Resource not found');
} catch (err) {
    if (err instanceof NotFoundError) {
        console.log(err.statusCode); // 404
    }
    console.log(err.name); // 'NotFoundError'
    console.log(err.message); // 'Resource not found'
}

In summary, creating custom error classes helps you manage specific types of errors more astutely. It allows clearer and more meaningful error messages and is a 'best practice' approach to handling exceptions in large codebases. However, be sure of correctly extending the Error class.

Have you considered how custom error classes could be beneficial to your current projects? How might they improve the clarity and reliability of your error handling processes?

Throwing and Handling Custom Errors

To effectively implement our custom-made error classes, understanding how to deploy the throw keyword is crucial. This is accompanied by try, catch, and finally blocks which together enable proficient error handling.

Throwing Custom Errors

Generating a manual error in JavaScript - of any type - can be accomplished using the throw keyword. When an error gets thrown, the flow of execution halts, and any try/catch blocks in the surrounding area are inspected for a relevant catch block to attend to the error.

For instance, create an instance of a hypothetical custom error called DatabaseConnectionError and throw it:

// Create a new instance of our custom error
let error = new DatabaseConnectionError('Unable to connect.');
// Throw the custom error
throw error;

Similar to built-in error classes, we can throw custom errors and handle them in catch blocks.

Handling Custom Errors

The try/catch/finally structure is utilized in JavaScript in order to deal with errors.

Here is the general structure:

try {
    // Code that could potentially throw an error
} catch (error) {
    // Code to handle the error
} finally {
    // Optional: Code that is executed regardless of the try/catch outcome
}

In the case of custom errors, we can use the instanceof keyword to differentiate between error types, enabling us to write bespoke error handling for different errors.

Here's an example:

try {
    // this line of code might generate an error
    queryDatabase();
} catch (error) {
    if (error instanceof DatabaseConnectionError) {
        // Log the custom error message and attempt to reconnect to the database
        console.error('Database connection error encountered:', error.message);
        reconnectToDatabase();
    } else {
        // Log any other error message
        console.error('Unknown error encountered:', error.message);
    }
} finally {
    // Possibly cleaning up any used resources
    cleanupResources();
}

In the provided code snippet, when a DatabaseConnectionError is encountered, the program will attempt to reconnect to the database. For all other errors, it just logs the error and continues.

Common Mistake when Throwing Errors

A frequent mistake made by developers when dealing with errors is checking the message attribute of the error object to differentiate between types. However, if the message gets changed, the error checking logic will break.

Incorrect way:

try {
    // this line of code might generate an error
    queryDatabase();
} catch (error) {
    if (error.message === 'Unable to connect.') {
        // Log the error message and attempt to reconnect to the database
        console.error('Database connection error encountered:', error.message);
        reconnectToDatabase();
    } else {
        // Log any other error message
        console.error('Unknown error encountered:', error.message);
    }
}

A more reliable way of checking for specific error types is using instanceof or even comparing the error.name property, both of which provide more reliable typifying.

Correct way:

try {
    // this line of code might generate an error
    queryDatabase();
} catch (error) {
    if (error instanceof DatabaseConnectionError || error.name === 'DatabaseConnectionError') {
        // Log the error message and attempt to reconnect to the database
        console.error('Database connection error encountered:', error.message);
        reconnectToDatabase();
    } else {
        // Log any other error message
        console.error('Unknown error encountered:', error.message);
    }
}

Lastly, remember that effectively incorporating custom error classes into your JavaScript involves creating an instance of your error class and throwing it. Coupled with proficient handling within try/catch blocks, this can make your error management logic much more robust and maintainable, and prepared for unexpected scenarios.

Take a moment to reflect on your current project. How can you integrate custom error classes and proficient error handling into it for an improved experience?

Practical Applications and Benefits of Custom Error Classes

Practical Applications and Benefits of Custom Error Classes

Custom error classes can be a game changer when it comes to debugging and managing your codebase. They extend the basic functionality of the native JavaScript Error object, offering the opportunity for more specific and meaningful error messaging and handling.

Improved Debugging And Error Messaging

Custom error classes can significantly improve your debugging process and get more meaningful error messages. By having specific types of errors, you can pinpoint exactly where in your code something is breaking. This makes the process of debugging quicker and more efficient. Here's an example of a custom error class doing just that:

class InvalidPasswordError extends Error {
    constructor(message = "Invalid Password") {
        super(message);
        this.name = 'InvalidPasswordError';
    }
}

try {
    let password = getUserPassword();
    if (!isValidPassword(password)) {
        throw new InvalidPasswordError();
    }
} catch (err) {
    if (err instanceof InvalidPasswordError) {
        console.error(`Caught InvalidPasswordError: ${err}`);
    }
    // other error handling code...
}

In this example, when the password is invalid, an InvalidPasswordError is thrown. This error is now distinguishable from other types of errors in the error handling code.

Modularity and Reusability

Creating custom error classes also supports the principles of modularity and reusability. You can define once and reuse it in various parts of your codebase. This allows you to maintain consistency and prevent repetition.

Common Mistake: One common pitfall developers often do is using only the native Error object. This may be sufficient for small, simple applications, but as your application grows, using only the native Error object may make the debugging process complicated due to lack of specific error types.

Correct Practice: Instead of using only the built-in Error object, define error classes according to the errors that can occur within your application:

class NotFoundError extends Error {
    constructor(message = "Not Found") {
        super(message);
        this.name = 'NotFoundError';
    }
}

By defining a custom NotFoundError, you can easily handle and debug errors related to missing resources.

Optimal Performance

While creating custom error classes might seem like an added complexity, it provides long-term performance benefits. Your system can handle different types of errors according to their class, thus reducing unnecessary checks and potentially unneeded operations. It enhances the computational performance of your app, as the error handling mechanism is more streamlined and efficient.

To conclude, custom error classes in JavaScript can significantly improve the experience of debugging, maintaining, and reading your code. This makes your codebase more maintainable and robust, leading to smoother development and enhancing software quality. Have you considered implementing custom error classes in your JavaScript code? If not, you might want to rethink your error handling strategies today.

Interaction with Node.js

In Node.js, creating and handling custom errors naturally weave into the system's approach to asynchronous and event-driven processing. It significantly boosts your debugging and error-handling process, bringing about straightforward tracking and clean code base.

To begin, let's look at a standard usage of custom error classes in Node.js:

class AuthenticationError extends Error {
    constructor(message) {
        super(message);
        this.name = 'AuthenticationError';
        this.code = 'EAUTH';
    }
}

module.exports = { AuthenticationError };

In this example, AuthenticationError is a custom error class to handle authentication failures. By exporting it with module.exports, we are availing the error class for use across files within your Node.js application.

Custom Errors and Express Error Handling

When working with the Express.js framework, one of the popular frameworks for Node.js applications, you could integrate custom errors into its error handling middleware.

const { AuthenticationError } = require('./errors');
const express = require('express');
const app = express();

app.use((req, res, next) => {
    if (!req.user) throw new AuthenticationError('User not authenticated');
    next();
});

app.use((err, req, res, next) => {
    if (err instanceof AuthenticationError)
        res.status(401).send(err.message);
    else
        next(err);
});

In this server script, we import our AuthenticationError and use it within an Express middleware function. If a user isn't authenticated, our middleware throws an AuthenticationError. Another middleware catches any instances of our custom error, responding with a specific status code and error message.

Common Coding Mistakes

A common coding mistake is to throw strings rather than error instances. The issue is that strings cannot hold stack trace information that assists in debugging.

Incorrect:

throw "A sample error message";

Correct:

throw new Error("A sample error message");

Another common mistake is throwing errors without a proper classification, making them harder to efficiently catch and handle.

Incorrect:

throw new Error('User not authenticated');

Correct:

throw new AuthenticationError('User not authenticated');

It's imperative to know that creating custom error classes doesn't excuse bad practices in error handling. Always follow the principle of catching errors where you can handle them.

Performance Impact and Debugging

Custom error classes provide a system for consistently identifying and managing specific errors. However, how does it impact performance and debugging?

Creating custom error classes doesn't negatively impact performance. It promotes better debugging as error types can be immediately identified from the error name or custom properties like code in our AuthenticationError example.

Finally, remember that the more granular your error classes are, the easier debugging becomes. But balance granularity with simplicity to maintain readable and manageable code.

Thought-provoking questions to consider: How can you effectively manage growing custom error classes in your Node.js applications? How will you document these custom error classes for your team's reference?

Summary

The article dives deep into the world of custom error classes in JavaScript. It begins by providing insights into the basic built-in error types, emphasizing that errors are object instances thrown with the keyword 'throw'. The article then proceeds to show us how to apply this to create custom error classes, avoiding common missteps such as not extending the built-in error class. The merits of custom error classes are numerous, including improved debugging, reusability, and better computational performance.

The piece also touches upon handling and throwing custom errors, incorporating try, catch, and finally keywords, and handling different errors individually. Interestingly, it also extends the use of custom error classes to the Node.js environment, demonstrating how custom error classes can conveniently integrate into Express.js and improve debugging processes. Challenging task: Create custom error classes for your current projects and evaluate how it impacts your debugging and error-handling process. Reflect on how the principles of modularity and reusability come into play and analyze the effect on performance.

Don't Get Left Behind:
The Top 5 Career-Ending Mistakes Software Developers Make
FREE Cheat Sheet for Software Developers