DEV Community

Cover image for Building a Better `AppError` Class in Node.js for Robust API Error Handling
Alifa Ara Heya
Alifa Ara Heya

Posted on

Building a Better `AppError` Class in Node.js for Robust API Error Handling

When you're building a Node.js API with Express, error handling is critical. You want responses to be:

✅ Consistent
✅ Predictable
✅ Meaningful for the client

But JavaScript’s built-in Error class falls short for APIs — it doesn’t carry the HTTP status code.

The AppError class extends the built-in Error to add a statusCode property. This allows you to create errors that carry their own status code, making your error handling much cleaner and more predictable.

Let’s build a powerful AppError class and integrate it cleanly into your Express API.


🚫 The Problem with the Default Error Class

Consider this typical controller logic:

// user.controller.ts
try {
  const user = await UserServices.findUserById(req.params.id);
  if (!user) {
    throw new Error('User not found');
  }
  res.json(user);
} catch (err) {
  res.status(500).json({ success: false, message: err.message });
}
Enter fullscreen mode Exit fullscreen mode

The issue?
Even though User not found is a 404, we send a 500 Internal Server Error — because there's no way to attach the right status code to the error itself.


✅ A First Draft: Creating AppError

Let’s define a simple custom error class:

// src/errorHelpers/appError.ts
class AppError extends Error {
  public statusCode: number;

  constructor(statusCode: number, message: string, stack = '') {
    super(message);
    this.statusCode = statusCode;

    if (stack) {
      this.stack = stack;
    } else {
      Error.captureStackTrace(this, this.constructor);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

🔍 Explanation: What’s Happening Behind the Scenes

AppError Breakdown:

The first line

class AppError extends Error {...
Enter fullscreen mode Exit fullscreen mode

declares a new class named AppError that inherits from the standard JavaScript Error class. This means an AppError instance will have all the properties and methods of a regular Error (like .message and .stack), plus any new ones we define.

The second line

    public statusCode: number;
Enter fullscreen mode Exit fullscreen mode

This declares a new public property on the class called statusCode. This is the key addition that will hold the HTTP status code (e.g., 400, 404, 500).

The third line

  constructor(statusCode: number, message: string, stack = '') {
Enter fullscreen mode Exit fullscreen mode

This is the constructor for the class. It's called whenever you create a new instance with new AppError(...). It takes three arguments:

  • statusCode: The HTTP status code for this error.
  • message: The human-readable error message.
  • stack = '': An optional stack trace. If you don't provide one, it will be generated automatically.
        super(message)
Enter fullscreen mode Exit fullscreen mode

This is crucial. It calls the constructor of the parent Error class and passes the message to it. This is what sets the error.message property correctly.

    this.statusCode = statusCode
Enter fullscreen mode Exit fullscreen mode

This line assigns the statusCode passed into the constructor to the statusCode property of the new AppError instance.

        if (stack) {
            this.stack = stack
        } else {
            Error.captureStackTrace(this, this. Constructor)
        }
Enter fullscreen mode Exit fullscreen mode

This block handles the stack trace.

If a stack string is provided to the constructor, it uses that.
Otherwise, it calls Error.captureStackTrace(this, this. Constructor). This is a standard Node.js V8 feature that generates a stack trace for the error. The second argument (this. Constructor) tells it to exclude the AppError constructor function from the stack trace, which makes your error logs cleaner and points directly to where the error was created.

Why this works:

  • this.statusCode = statusCode: We now attach the right HTTP status code.
  • Error.captureStackTrace(...): This is a standard Node.js practice. It captures the stack trace, excluding the constructor function itself, which makes for cleaner error logs.

💡 How to Use AppError

1️⃣ Throwing it in services or controllers:

// user.service.ts
import { AppError } from '../errorHelpers/appError';
import httpStatus from 'http-status-codes';

const findUserById = async (id: string) => {
  const user = await User.findById(id);
  if (!user) {
    throw new AppError(httpStatus.NOT_FOUND, 'User not found!');
  }
  return user;
};
Enter fullscreen mode Exit fullscreen mode

2️⃣ Handling it globally:

This custom error becomes powerful when used with globalErrorHandler.

// middlewares/globalErrorHandler.ts

export const globalErrorHandler = (err, req, res, next) => {
  const statusCode = err.statusCode || 500;
  const message = err.message || 'Something went wrong!';

  res.status(statusCode).json({
    success: false,
    message,
    stack: process.env.NODE_ENV === 'development' ? err.stack : undefined,
  });
};
Enter fullscreen mode Exit fullscreen mode

✅ Now the response reflects the real nature of the error — 404 for missing user, 403 for unauthorized, etc.


🧪 Bonus: Cleaner Code, Better API

With AppError, your services are responsible for context, and your global error handler is consistent.

Your API becomes:

  • ✅ Easier to maintain
  • ✅ More readable
  • ✅ Scalable with different error types (ValidationError, DatabaseError, etc.)

🎉 Conclusion

By creating an AppError class:

  • You decouple status code logic from the global handler
  • You make services more expressive and controllers leaner
  • You set the foundation for robust, predictable error handling

Pro tip: You can even extend AppError into specific subclasses like ValidationError, AuthError, etc.


📌 TL;DR

Problem Solution
No HTTP status in Error Add statusCode in AppError
Messy stack traces Use Error.captureStackTrace
Guessing error codes Define them at the throw site

By implementing this pattern, you've made your error handling more structured, expressive, and easier to manage.

Happy coding! 🚀

Top comments (0)