Custom Error Class and Centralized Error Handling in NodeJS
When building APIs, handling errors gracefully is crucial for maintaining a clean codebase and providing meaningful feedback to the client. Without a proper error-handling mechanism, it becomes difficult to distinguish between different types of errors, especially when each error needs to return a specific HTTP status code.
For example, a validation error should return a 400 (Bad Request), while an authorization issue requires a 401 (Unauthorized). If you rely on generic error messages, your application might end up sending the wrong status codes, leading to confusion for both developers and clients.
Here is an example of how most of people handles errors and send response.
exports.findAUser = async (req, res) => {
try {
const {id} = req.params;
let user = await User.findById(id);
if (!user) {
// send error in API
res.status(404).send("User not found");
}
// send success with user data
res.status(200).json(user);
} catch (err) {
// send error in API
res.status(500).send(err.message);
}
}
There is no potential issue in this code snippet but we can improve the error handling here.
So, my solution is to make a Custom Error Class to throw all errors in catch with HTTP status code and data. I will use catch to send error response in API and we can also make 2 helpers for success and error to send response in API.
Let’s make a ThrowError
class in NodeJS that extends the built-in Error
class. It provides a structured way to handle errors by attaching additional properties such as statusCode
and data
.
class ThrowError extends Error {
constructor(msg = 'Something went wrong', statusCode = 400, data = {}) {
super(msg);
this.statusCode = statusCode;
this.data = data;
}
}
module.exports = ThrowError;
Class Explanation:
By extending Error
, the ThrowError
class inherits all standard error properties and behaviors, such as the message
and stack
. This makes it compatible with existing error-handling mechanisms in Node.js.
The constructor provides default values:
msg
: Defaults to"Something went wrong"
if no custom message is passed.statusCode
: Defaults to400
(Bad Request), a common error code for client-side issues.data
: Defaults to an empty object{}
, allowing developers to attach additional details about the error, such as validation issues or debug information.
Usage:
// import the class
const ThrowError = require('@utils/ThrowError');
// ...
// ...
let user = await User.findById(id);
if (!user) {
// send 404 error in API
throw new ThrowError('User not found', 404, {id: id});
}
if (!user.isAuthenticated) {
// send 401 error in API
throw new ThrowError('User not authenticated', 401, {id: id});
}
Now, let’s make helpers to send API response
const ThrowError = require('@utils/throwError');
const sendSuccessResponse = (response, data = {}, message = "Success", statusCode = 200) => {
return response.status(statusCode).json({success: true, message, data});
}
const sendErrorResponse = (response, error) => {
let statusCode = 400;
if (error instanceof ThrowError) statusCode = error.statusCode;
response.status(statusCode).json({success: false, message: error.message, data: error.data || {}});
}
module.exports = {sendSuccessResponse, sendErrorResponse};
Final Code Block:
The code now looks more clean and centralized. This approach will also help to improve debugging.
exports.findAUser = async (req, res) => {
try {
const {id} = req.params;
let user = await User.findById(id);
if (!user) {
throw new ThrowError('User not found', 404, {id: id});
}
sendSuccessResponse(res, user);
} catch (err) {
sendErrorResponse(res, err);
}
}
Benefits of this approach:
- Attach HTTP Status Codes to Errors
A custom error class allows you to define a status code along with the error message. This makes it easy to send accurate responses to the client without additional logic in everycatch
block. - Centralize Error Logic
Instead of manually setting status codes and messages everywhere, a custom class keeps this logic centralized. This reduces boilerplate code and makes your application easier to maintain. - Consistent Error Structure
Clients consuming your API expect consistent error responses. A custom class ensures that all errors follow the same format, making it easier for developers to debug and for clients to parse. - Improve Debugging
By extending the built-inError
class, you can also include additional metadata such aserror types
,timestamps
, orstack traces
, which are invaluable during debugging.
By leveraging a custom error class, you bring clarity and professionalism to your error-handling strategy, ensuring your APIs are robust, predictable, and easier to work with. This not only makes your development experience smoother but also builds trust with the developers who rely on your API.