Error Handling Strategy
Relevant source files
The following files were used as context for generating this wiki page:
The backend of the application employs a centralized and standardized error handling strategy using Express.js middleware. This approach ensures that all errors, whether operational or programmer-generated, are caught and processed consistently, resulting in predictable JSON responses for the client. The core of this strategy revolves around a custom ErrorHandler class, a wrapper for asynchronous route handlers, and a final error-handling middleware that formats the output.
This system is designed to keep controller logic clean by abstracting away repetitive try...catch blocks and to provide specific, meaningful error messages and HTTP status codes for different failure scenarios, such as invalid input, database errors, or authentication issues.
Core Components
The error handling mechanism is built upon three main components: the ErrorHandler class, the catchAsyncErrors utility, and the main error middleware function.
ErrorHandler Class
This is a custom error class that extends the native JavaScript Error class. Its purpose is to create operational errors with a specific HTTP status code and a custom message. This allows developers to throw errors from anywhere in the application (e.g., controllers, services) and have them be correctly interpreted by the final error middleware.
Sources: backend/utils/ErrorHandler.js
class ErrorHandler extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
Error.captureStackTrace(this, this.constructor);
}
}
Sources: backend/utils/ErrorHandler.js:1-9
The diagram below shows the simple structure of the ErrorHandler class.
Sources: backend/utils/ErrorHandler.js
catchAsyncErrors Middleware
This is a higher-order function that wraps asynchronous Express route handlers. Its primary role is to catch any errors that occur within an async function (i.e., rejected Promises) and pass them to Express's next() function. This prevents the server from crashing due to unhandled promise rejections and ensures that all async errors are funneled to the centralized error handler.
module.exports = (theFunc) => (req, res, next) => {
Promise.resolve(theFunc(req, res, next)).catch(next);
};
Sources: backend/middleware/catchAsyncErrors.js:1-3
The flow of this wrapper is illustrated below. It executes the provided function and attaches a .catch(next) to its resulting promise.
Centralized Error Middleware
This is the final and most critical piece of the error handling chain. It is an Express error-handling middleware function (distinguished by its four arguments: err, req, res, next) that is registered at the end of the middleware stack in app.js. It receives all errors passed to next() from any preceding middleware or route handler.
The middleware's job is to:
- Set a default status code (500) and message if none are provided.
- Inspect the error object to identify specific types of known errors (e.g., invalid MongoDB ID, duplicate key, JWT errors).
- For these specific errors, it creates a new
ErrorHandlerinstance with a more user-friendly message and a precise status code (usually 400). - Send a standardized JSON error response to the client with the format
{ success: false, message: err.message }.
Sources: backend/app.js:132, backend/middleware/error.js
Specific Error Handling
The middleware explicitly handles several common error types.
| Error Name / Code | Condition | Formatted Message | Status Code |
|---|---|---|---|
CastError | A malformed MongoDB ObjectId is used. | Không tìm thấy tài nguyên với id này.. Không hợp lệ ${err.path} | 400 |
11000 (code) | A duplicate key error from MongoDB. | Khóa đã nhập trùng lặp ${Object.keys(err.keyValue)} | 400 |
JsonWebTokenError | The JWT is malformed or invalid. | url của bạn không hợp lệ vui lòng thử lại | 400 |
TokenExpiredError | The JWT has expired. | url của bạn không hợp lệ vui lòng thử lại ! | 400 |
Sources: backend/middleware/error.js:7-28
End-to-End Flow
The following diagram illustrates the complete error handling flow, from a client request to the final error response.
Usage in Controllers
Controllers use catchAsyncErrors to wrap their main logic and new ErrorHandler(...) to trigger specific, operational errors. This keeps the controller code focused on business logic.
// Example from backend/controller/user.js
router.post(
'/login-user',
catchAsyncErrors(async (req, res, next) => {
// ...
const user = await User.findOne({ where: { email } });
if (!user) {
// Trigger a 400 error
return next(new ErrorHandler('Người dùng không tồn tại!', 400));
}
const isPasswordValid = await user.comparePassword(password);
if (!isPasswordValid) {
// Trigger a 400 error
return next(new ErrorHandler('Vui lòng cung cấp thông tin chính xác!', 400));
}
sendToken(user, 201, res);
// ...
}),
);
Sources: backend/controller/user.js:133-151, backend/middleware/catchAsyncErrors.js, backend/utils/ErrorHandler.js
Integration with Input Validation
The error handling strategy is also integrated with the input validation middleware (express-validator). If validation fails, the validate middleware constructs a new ErrorHandler instance with a 400 status code and passes it to next(), which is then caught by the central error handler.
// backend/middleware/validator.js
exports.validate = (validations) => {
return async (req, res, next) => {
// ...
const errors = validationResult(req);
if (errors.isEmpty()) {
return next();
}
const errorMessages = errors.array().map(err => err.msg).join(', ');
// Pass a formatted error to the central handler
return next(new ErrorHandler(errorMessages, 400));
};
};
Sources: backend/middleware/validator.js:5-16
Conclusion
The project's error handling strategy is robust, centralized, and scalable. By combining a custom ErrorHandler class, an async wrapper utility, and a final middleware layer, it provides a consistent mechanism for managing and reporting errors. This design simplifies controller logic, improves maintainability, and ensures that the frontend receives clear and predictable error responses, which is crucial for a good user experience.