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 });
}
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);
}
}
}
🔍 Explanation: What’s Happening Behind the Scenes
AppError
Breakdown:
The first line
class AppError extends Error {...
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;
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 = '') {
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)
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
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)
}
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;
};
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,
});
};
✅ 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 likeValidationError
,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)