Shop Flow implements a comprehensive error handling system based on custom error classes that extend a base CustomError class. The system provides consistent error responses, proper HTTP status codes, and localized error messages in Persian.
The CustomError abstract class serves as the foundation for all custom errors:
export abstract class CustomError extends Error {
constructor(message: string) {
super(message);
Object.setPrototypeOf(this, CustomError.prototype);
}
abstract statusCode: number;
abstract serializeErrors: () => {
field: string | null;
message: string;
}[];
}Key Features:
- Abstract Base: Ensures consistent interface across all error types
- Prototype Fix: Handles TypeScript inheritance issues with built-in classes
- Serialization: Standardized error response format
- Status Codes: HTTP status code enforcement
All errors follow a consistent JSON response structure:
{
"status": "error",
"errors": [
{
"field": "email" | null,
"message": "Error message in Persian"
}
]
}File: src/errors/bad-request-error.ts
Purpose: General client-side errors, invalid requests
export class BadRequestError extends CustomError {
statusCode = 400;
constructor(public override message: string) {
super(message);
Object.setPrototypeOf(this, BadRequestError.prototype);
}
}Usage Examples:
- Invalid input data
- Missing required parameters
- Business rule violations
File: src/errors/not-authorized-error.ts
Purpose: Authentication failures
export class NotAuthorizedError extends CustomError {
statusCode = 401;
constructor(message: string) {
super(message);
Object.setPrototypeOf(this, NotAuthorizedError.prototype);
}
}Usage Examples:
- Invalid credentials
- Expired tokens
- Missing authentication
- User account disabled
Common Messages:
"شما وارد نشده اید! لطفا برای دسترسی وارد شوید"- Not logged in"کاربر متعلق به این توکن دیگر وجود ندارد!"- User no longer exists"کاربری که به این ایمیل مرتبط است غیرفعال شده!"- Account disabled
File: src/errors/forbidden-error.ts
Purpose: Authorization failures (authenticated but insufficient permissions)
export class ForbiddenError extends CustomError {
statusCode = 403;
constructor(message: string) {
super(message);
Object.setPrototypeOf(this, ForbiddenError.prototype);
}
}Usage Examples:
- Role-based access control violations
- Resource ownership restrictions
- Admin-only operations
Common Messages:
"شما اجازه انجام این عمل را ندارید!"- Insufficient permissions
File: src/errors/not-found-error.ts
Purpose: Resource not found errors
export class NotFoundError extends CustomError {
statusCode = 404;
constructor(message: string) {
super(message);
Object.setPrototypeOf(this, NotFoundError.prototype);
}
}Usage Examples:
- Non-existent resources
- Invalid IDs
- Deleted resources
- Route not found
Common Messages:
"هیچ محصولی با این شناسه یافت نشد"- Product not found"صفحه مورد نظر یافت نشد"- Page not found"این صفحه وجود ندارد"- Pagination page doesn't exist
File: src/errors/unprocessable-entity-error.ts
Purpose: Semantic errors in well-formed requests
export class UnprocessableEntityError extends CustomError {
statusCode = 422;
constructor(public override message: string) {
super(message);
Object.setPrototypeOf(this, UnprocessableEntityError.prototype);
}
}Usage Examples:
- Business logic violations
- Data consistency errors
- Complex validation failures
File: src/errors/request-validation-error.ts
Purpose: Input validation errors from express-validator
export class RequestValidationError extends CustomError {
statusCode = 400;
constructor(public errors: ValidationError[]) {
super("پارامترهای درخواست نامعتبر هستند!");
this.errors = errors;
}
serializeErrors = () => {
return this.errors.map((error) => ({
field: error.type === "field" ? error.path : null,
message: error.msg,
}));
};
}Special Features:
- Multiple Errors: Can contain multiple validation errors
- Field Mapping: Maps errors to specific input fields
- Express-Validator Integration: Works with validation middleware
File: src/errors/internal-server-error.ts
Purpose: Server-side errors and unexpected failures
export class InternalServerError extends CustomError {
statusCode = 500;
constructor(message: string) {
super(message);
Object.setPrototypeOf(this, InternalServerError.prototype);
}
}Usage Examples:
- Database connection failures
- External service errors
- Email sending failures
- Unexpected system errors
The errorHandler middleware provides centralized error processing:
export const errorHandler: ErrorRequestHandler = (err, req, res, next) => {
// Handle Mongoose CastError
if (err.name === "CastError") {
return res.status(400).send({
status: "error",
errors: [
{
field: err.path,
message: "شناسه کاربر معتبر نیست",
},
],
});
}
// Handle MongoDB duplicate key error
if (err.code === 11000 && err.keyPattern?.email) {
return res.status(400).send({
status: "error",
errors: [
{
field: err.path,
message: "این ایمیل قبلا استفاده شده است",
},
],
});
}
// Handle custom errors
if (err instanceof CustomError) {
return res.status(err.statusCode).send({
status: "error",
errors: err.serializeErrors(),
});
}
// Handle unexpected errors
console.error(err);
return res.status(500).send({
status: "error",
errors: [
{
field: null,
message: "یک چیزی خیلی اشتباه پیش رفت",
},
],
});
};-
Mongoose Error Handling:
- CastError: Invalid ObjectId format
- Duplicate Key: Unique constraint violations
-
Custom Error Processing:
- Automatic status code mapping
- Consistent error serialization
- Localized error messages
-
Fallback Handling:
- Catches unexpected errors
- Logs errors for debugging
- Returns generic error message
-
Security:
- Prevents error information leakage
- Sanitizes error responses
- Logs sensitive errors server-side
// In ProductService
async getProductById(id: string): Promise<IProductDoc> {
const product = await this.productRepository.getOne(id);
if (!product) {
throw new NotFoundError("هیچ محصولی با این شناسه یافت نشد");
}
return product;
}// In auth middleware
const protect: RequestHandler = async (req, res, next) => {
if (!token) {
throw new NotAuthorizedError(
"شما وارد نشده اید! لطفا برای دسترسی وارد شوید",
);
}
const user = await userRepository.findById(decoded.id);
if (!user) {
throw new NotAuthorizedError("کاربر متعلق به این توکن دیگر وجود ندارد!");
}
if (!user.active) {
throw new NotAuthorizedError(
"کاربری که به این ایمیل مرتبط است غیرفعال شده!",
);
}
};// In restrictTo middleware
const restrictTo = (...roles: string[]): RequestHandler => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
throw new ForbiddenError("شما اجازه انجام این عمل را ندارید!");
}
return next();
};
};// In validateRequest middleware
export const validateRequest: RequestHandler = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
throw new RequestValidationError(errors.array());
}
next();
};- All error messages are in Persian for user-facing errors
- Technical errors are logged in English for developers
- Error messages are descriptive but not revealing sensitive information
- 400: Client errors (bad request, validation)
- 401: Authentication required
- 403: Forbidden (insufficient permissions)
- 404: Resource not found
- 422: Unprocessable entity (business logic errors)
- 500: Server errors
- Unexpected errors are logged to console
- Sensitive information is not exposed in responses
- Error context is preserved for debugging
The project uses express-async-errors to automatically catch async errors:
// No need for try-catch blocks in route handlers
app.get("/users/:id", async (req, res) => {
const user = await userService.getUserById(req.params.id);
// If getUserById throws an error, it's automatically caught
res.json(user);
});- Errors are caught at the appropriate layer
- Business logic errors in service layer
- Validation errors in middleware
- Authentication/authorization errors in middleware
// Routes automatically handle thrown errors
router.get("/products/:id", async (req, res) => {
// This will automatically be caught by error handler
const product = await productService.getProductById(req.params.id);
res.json({ status: "success", data: product });
});// Error handling middleware is registered last
app.use("/api/products", productRouter);
app.use("/api/users", userRouter);
// 404 handler for unmatched routes
app.all("*", () => {
throw new NotFoundError("صفحه مورد نظر یافت نشد");
});
// Global error handler (must be last)
app.use(errorHandler);// Testing error responses
describe("Product API", () => {
it("should return 404 for non-existent product", async () => {
const response = await request(app)
.get("/api/products/invalid-id")
.expect(404);
expect(response.body).toEqual({
status: "error",
errors: [
{
field: null,
message: "هیچ محصولی با این شناسه یافت نشد",
},
],
});
});
});- Error Logging: Implement proper logging service (Winston, etc.)
- Error Tracking: Use services like Sentry for error monitoring
- Metrics: Track error rates and types
- Alerting: Set up alerts for critical errors
- Stack Traces: Full stack traces in development mode
- Error Details: Detailed error information for debugging
- Console Logging: Errors logged to console for immediate feedback
All user-facing error messages are in Persian:
- Authentication:
"شما وارد نشده اید! لطفا برای دسترسی وارد شوید" - Authorization:
"شما اجازه انجام این عمل را ندارید!" - Not Found:
"هیچ محصولی با این شناسه یافت نشد" - Validation:
"پارامترهای درخواست نامعتبر هستند!" - Server Error:
"یک چیزی خیلی اشتباه پیش رفت"
Internal error messages and logs remain in English for developer clarity.