DEV Community

Cover image for Building a Simple and Effective Error-Handling System in Node.js
itsmejanmicko
itsmejanmicko

Posted on

Building a Simple and Effective Error-Handling System in Node.js

When building web applications, things don’t always go as planned. Users might try invalid operations, data might be missing, or unexpected bugs could arise. Handling these errors gracefully is essential for creating a reliable application and providing a good user experience.

In this post, I’ll show you how to implement a simple and effective error-handling system in your Node.js application. Let’s dive in! 🚀

The Basics of Error Handling

In JavaScript, an error is typically represented as an instance of the Error class. When something goes wrong, you can "throw" an error, and your application can catch it to respond accordingly. However, in larger applications, managing errors with just the basic Error class can get messy.

This is where custom error classes come in handy!
Creating a Custom Error Class

Here’s a class called AppError that we can use to define structured and meaningful errors:

export class AppError extends Error {
    constructor(name, httpCode, description, isOperational, errors = []) {
        super(description); // Call the parent class (Error) constructor
        this.name = name; // Name of the error (e.g., ValidationError)
        this.httpCode = httpCode; // HTTP status code (e.g., 400, 404, 500)
        this.isOperational = isOperational; // Flag to distinguish between operational and system errors
        this.errors = errors; // Optional array of detailed error messages
    }
}

Enter fullscreen mode Exit fullscreen mode

How Does It Work?

  • name: Describes the type of error, like ValidationError or NotFoundError.
  • httpCode: Sets an appropriate HTTP status code, such as 400 for bad requests or 500 for server errors.
  • description: Provides a user-friendly message describing the error.
  • isOperational: A boolean flag to indicate if the error was expected (e.g., user input issues) or unexpected (e.g., bugs).
  • errors: An optional list to include more specific details about the error (useful for validation errors).

A Simple Error-Handling Middleware

Now that we have our custom error class, we need a way to handle these errors in our app. Enter middleware:

export const errorHandler = (err, req, res, next) => {
    if (err instanceof AppError) {
        // Handle operational errors
        res.status(err.httpCode).json({
            success: false,
            error: err.name,
            message: err.message,
            errors: err.errors || [],
        });
    } else {
        // Handle unexpected errors
        console.error('Unexpected Error:', err.stack); // Log the stack trace for debugging
        res.status(500).json({
            success: false,
            error: 'InternalServerError',
            message: 'Something went wrong. Please try again later.',
        });
    }
};

Enter fullscreen mode Exit fullscreen mode

What Does This Do?
Checks the error type:
If the error is an instance of AppError, it’s treated as an operational error. These are the errors you anticipate (e.g., validation issues or missing resources).
If it’s not an AppError, it’s treated as an unexpected error. These might be bugs in your code or something you didn’t plan for.

Responds with the right status code and message:
    For AppError instances, it sends a structured JSON response containing the error details.
    For unexpected errors, it sends a generic 500 response to the user and logs the details for debugging.
Enter fullscreen mode Exit fullscreen mode

Putting It All Together

Imagine you have a route in your Express app where users submit a form, but some fields are missing. You can use your AppError class like this:

app.post('/submit-form', (req, res, next) => {
    const { name, email } = req.body;

    if (!name || !email) {
        const errors = [];
        if (!name) errors.push({ field: 'name', message: 'Name is required.' });
        if (!email) errors.push({ field: 'email', message: 'Email is required.' });

        return next(new AppError('ValidationError', 400, 'Invalid input data.', true, errors));
    }

    res.json({ success: true, message: 'Form submitted successfully!' });
});

// Register the error-handling middleware
app.use(errorHandler);

Enter fullscreen mode Exit fullscreen mode

What Happens When There’s an Error?
If the name or email is missing, the AppError instance is passed to the next function.
The errorHandler middleware catches it and sends a structured JSON response:

{
    "success": false,
    "error": "ValidationError",
    "message": "Invalid input data.",
    "errors": [
        { "field": "name", "message": "Name is required." },
        { "field": "email", "message": "Email is required." }
    ]
}

Enter fullscreen mode Exit fullscreen mode

Why Use This Pattern?

  • Clarity: Custom error classes make your errors more descriptive and easier to debug.
  • Consistency: Every error follows a predictable structure, making it easier for frontend developers to handle.
  • Separation of Concerns: Your route handlers focus on the logic, while the middleware handles the error response.

Conclusion
Error handling doesn’t have to be complex! By using a custom error class and middleware, you can build a robust and beginner-friendly system that keeps your app reliable and your users happy.

What do you think of this approach? Do you have any tips or tricks for handling errors in Node.js? Let me know in the comments below!

Top comments (0)