Proper error handling prevents crashes and improves debugging. Here's how to handle errors effectively in Node.js.
Custom Error Classes#
1// Base application error
2class AppError extends Error {
3 public readonly statusCode: number;
4 public readonly isOperational: boolean;
5 public readonly code: string;
6
7 constructor(
8 message: string,
9 statusCode: number = 500,
10 code: string = 'INTERNAL_ERROR',
11 isOperational: boolean = true
12 ) {
13 super(message);
14 this.statusCode = statusCode;
15 this.code = code;
16 this.isOperational = isOperational;
17
18 Error.captureStackTrace(this, this.constructor);
19 Object.setPrototypeOf(this, new.target.prototype);
20 }
21}
22
23// Specific error types
24class ValidationError extends AppError {
25 public readonly errors: Record<string, string>;
26
27 constructor(errors: Record<string, string>) {
28 super('Validation failed', 400, 'VALIDATION_ERROR');
29 this.errors = errors;
30 }
31}
32
33class NotFoundError extends AppError {
34 constructor(resource: string) {
35 super(`${resource} not found`, 404, 'NOT_FOUND');
36 }
37}
38
39class UnauthorizedError extends AppError {
40 constructor(message: string = 'Unauthorized') {
41 super(message, 401, 'UNAUTHORIZED');
42 }
43}
44
45class ForbiddenError extends AppError {
46 constructor(message: string = 'Forbidden') {
47 super(message, 403, 'FORBIDDEN');
48 }
49}
50
51class ConflictError extends AppError {
52 constructor(message: string) {
53 super(message, 409, 'CONFLICT');
54 }
55}
56
57class RateLimitError extends AppError {
58 public readonly retryAfter: number;
59
60 constructor(retryAfter: number = 60) {
61 super('Too many requests', 429, 'RATE_LIMIT');
62 this.retryAfter = retryAfter;
63 }
64}Async Error Handling#
1// Async wrapper for Express routes
2const asyncHandler = (
3 fn: (req: Request, res: Response, next: NextFunction) => Promise<any>
4) => {
5 return (req: Request, res: Response, next: NextFunction) => {
6 Promise.resolve(fn(req, res, next)).catch(next);
7 };
8};
9
10// Usage
11app.get('/users/:id', asyncHandler(async (req, res) => {
12 const user = await userService.findById(req.params.id);
13
14 if (!user) {
15 throw new NotFoundError('User');
16 }
17
18 res.json(user);
19}));
20
21// Alternative: express-async-errors
22import 'express-async-errors';
23
24// Now async errors are caught automatically
25app.get('/users/:id', async (req, res) => {
26 const user = await userService.findById(req.params.id);
27
28 if (!user) {
29 throw new NotFoundError('User');
30 }
31
32 res.json(user);
33});Promise Error Handling#
1// Proper promise chain error handling
2async function processOrder(orderId: string) {
3 try {
4 const order = await getOrder(orderId);
5 const payment = await processPayment(order);
6 const shipment = await createShipment(order, payment);
7
8 return { order, payment, shipment };
9 } catch (error) {
10 if (error instanceof PaymentError) {
11 await refundOrder(orderId);
12 throw new AppError('Payment failed', 402, 'PAYMENT_FAILED');
13 }
14
15 if (error instanceof ShipmentError) {
16 await cancelPayment(orderId);
17 throw new AppError('Shipment creation failed', 500, 'SHIPMENT_FAILED');
18 }
19
20 throw error;
21 }
22}
23
24// Handle multiple promises
25async function fetchAllData() {
26 const [users, posts, comments] = await Promise.all([
27 fetchUsers().catch(() => []), // Return empty on failure
28 fetchPosts().catch(() => []),
29 fetchComments().catch(() => []),
30 ]);
31
32 return { users, posts, comments };
33}
34
35// With error details
36async function fetchWithErrors() {
37 const results = await Promise.allSettled([
38 fetchUsers(),
39 fetchPosts(),
40 fetchComments(),
41 ]);
42
43 const errors = results
44 .filter((r): r is PromiseRejectedResult => r.status === 'rejected')
45 .map(r => r.reason);
46
47 if (errors.length > 0) {
48 console.error('Some requests failed:', errors);
49 }
50
51 const data = results
52 .filter((r): r is PromiseFulfilledResult<any> => r.status === 'fulfilled')
53 .map(r => r.value);
54
55 return data;
56}Centralized Error Handler#
1// errorHandler.ts
2interface ErrorResponse {
3 success: false;
4 error: {
5 code: string;
6 message: string;
7 details?: any;
8 };
9}
10
11function errorHandler(
12 err: Error,
13 req: Request,
14 res: Response,
15 next: NextFunction
16) {
17 // Log error
18 console.error('Error:', {
19 message: err.message,
20 stack: err.stack,
21 path: req.path,
22 method: req.method,
23 body: req.body,
24 user: req.user?.id,
25 });
26
27 // Handle known errors
28 if (err instanceof AppError) {
29 const response: ErrorResponse = {
30 success: false,
31 error: {
32 code: err.code,
33 message: err.message,
34 },
35 };
36
37 if (err instanceof ValidationError) {
38 response.error.details = err.errors;
39 }
40
41 return res.status(err.statusCode).json(response);
42 }
43
44 // Handle Prisma errors
45 if (err.name === 'PrismaClientKnownRequestError') {
46 const prismaError = err as any;
47
48 if (prismaError.code === 'P2002') {
49 return res.status(409).json({
50 success: false,
51 error: {
52 code: 'DUPLICATE_ENTRY',
53 message: 'A record with this value already exists',
54 details: prismaError.meta?.target,
55 },
56 });
57 }
58
59 if (prismaError.code === 'P2025') {
60 return res.status(404).json({
61 success: false,
62 error: {
63 code: 'NOT_FOUND',
64 message: 'Record not found',
65 },
66 });
67 }
68 }
69
70 // Handle JWT errors
71 if (err.name === 'JsonWebTokenError') {
72 return res.status(401).json({
73 success: false,
74 error: {
75 code: 'INVALID_TOKEN',
76 message: 'Invalid authentication token',
77 },
78 });
79 }
80
81 if (err.name === 'TokenExpiredError') {
82 return res.status(401).json({
83 success: false,
84 error: {
85 code: 'TOKEN_EXPIRED',
86 message: 'Authentication token has expired',
87 },
88 });
89 }
90
91 // Handle validation errors (Joi, Zod, etc.)
92 if (err.name === 'ZodError') {
93 return res.status(400).json({
94 success: false,
95 error: {
96 code: 'VALIDATION_ERROR',
97 message: 'Invalid request data',
98 details: (err as any).errors,
99 },
100 });
101 }
102
103 // Default error
104 const statusCode = (err as any).statusCode || 500;
105 const message = process.env.NODE_ENV === 'production'
106 ? 'An unexpected error occurred'
107 : err.message;
108
109 res.status(statusCode).json({
110 success: false,
111 error: {
112 code: 'INTERNAL_ERROR',
113 message,
114 },
115 });
116}
117
118// Register error handler last
119app.use(errorHandler);Unhandled Errors#
1// Global error handlers
2process.on('uncaughtException', (error: Error) => {
3 console.error('Uncaught Exception:', error);
4
5 // Log to monitoring service
6 logErrorToService(error);
7
8 // Graceful shutdown
9 process.exit(1);
10});
11
12process.on('unhandledRejection', (reason: any, promise: Promise<any>) => {
13 console.error('Unhandled Rejection at:', promise, 'reason:', reason);
14
15 // Log to monitoring service
16 logErrorToService(reason);
17
18 // Optionally exit
19 // process.exit(1);
20});
21
22// Graceful shutdown
23process.on('SIGTERM', async () => {
24 console.log('SIGTERM received. Shutting down gracefully...');
25
26 // Close database connections
27 await prisma.$disconnect();
28
29 // Close server
30 server.close(() => {
31 console.log('Server closed');
32 process.exit(0);
33 });
34
35 // Force close after timeout
36 setTimeout(() => {
37 console.error('Forced shutdown');
38 process.exit(1);
39 }, 10000);
40});Service Layer Error Handling#
1// userService.ts
2class UserService {
3 async findById(id: string): Promise<User> {
4 const user = await prisma.user.findUnique({
5 where: { id },
6 });
7
8 if (!user) {
9 throw new NotFoundError('User');
10 }
11
12 return user;
13 }
14
15 async create(data: CreateUserDTO): Promise<User> {
16 // Validate
17 const existingUser = await prisma.user.findUnique({
18 where: { email: data.email },
19 });
20
21 if (existingUser) {
22 throw new ConflictError('Email already registered');
23 }
24
25 // Validate password strength
26 if (!isStrongPassword(data.password)) {
27 throw new ValidationError({
28 password: 'Password must be at least 8 characters with a number and special character',
29 });
30 }
31
32 try {
33 const hashedPassword = await hashPassword(data.password);
34
35 return await prisma.user.create({
36 data: {
37 ...data,
38 password: hashedPassword,
39 },
40 });
41 } catch (error) {
42 // Handle specific database errors
43 throw new AppError('Failed to create user', 500, 'USER_CREATE_FAILED');
44 }
45 }
46
47 async update(id: string, data: UpdateUserDTO): Promise<User> {
48 await this.findById(id); // Throws if not found
49
50 try {
51 return await prisma.user.update({
52 where: { id },
53 data,
54 });
55 } catch (error) {
56 if (error instanceof NotFoundError) {
57 throw error;
58 }
59 throw new AppError('Failed to update user', 500, 'USER_UPDATE_FAILED');
60 }
61 }
62}Try-Catch Patterns#
1// Result type pattern
2type Result<T, E = Error> =
3 | { success: true; data: T }
4 | { success: false; error: E };
5
6async function safeAsync<T>(
7 promise: Promise<T>
8): Promise<Result<T>> {
9 try {
10 const data = await promise;
11 return { success: true, data };
12 } catch (error) {
13 return { success: false, error: error as Error };
14 }
15}
16
17// Usage
18async function processUser(id: string) {
19 const result = await safeAsync(userService.findById(id));
20
21 if (!result.success) {
22 console.error('Failed to find user:', result.error);
23 return null;
24 }
25
26 return result.data;
27}
28
29// Multiple operations with cleanup
30async function createUserWithProfile(data: CreateUserData) {
31 let user: User | null = null;
32
33 try {
34 user = await userService.create(data.user);
35 const profile = await profileService.create(user.id, data.profile);
36
37 return { user, profile };
38 } catch (error) {
39 // Cleanup on failure
40 if (user) {
41 await userService.delete(user.id).catch(() => {});
42 }
43 throw error;
44 }
45}Logging Errors#
1// logger.ts
2import winston from 'winston';
3
4const logger = winston.createLogger({
5 level: process.env.LOG_LEVEL || 'info',
6 format: winston.format.combine(
7 winston.format.timestamp(),
8 winston.format.errors({ stack: true }),
9 winston.format.json()
10 ),
11 transports: [
12 new winston.transports.File({ filename: 'error.log', level: 'error' }),
13 new winston.transports.File({ filename: 'combined.log' }),
14 ],
15});
16
17if (process.env.NODE_ENV !== 'production') {
18 logger.add(new winston.transports.Console({
19 format: winston.format.simple(),
20 }));
21}
22
23// Error logging utility
24function logError(error: Error, context?: Record<string, any>) {
25 const errorInfo = {
26 name: error.name,
27 message: error.message,
28 stack: error.stack,
29 code: (error as AppError).code,
30 statusCode: (error as AppError).statusCode,
31 ...context,
32 };
33
34 if (error instanceof AppError && error.isOperational) {
35 logger.warn('Operational error', errorInfo);
36 } else {
37 logger.error('System error', errorInfo);
38 }
39}
40
41// In error handler
42function errorHandler(err: Error, req: Request, res: Response, next: NextFunction) {
43 logError(err, {
44 path: req.path,
45 method: req.method,
46 userId: req.user?.id,
47 requestId: req.headers['x-request-id'],
48 });
49
50 // ... rest of error handling
51}Testing Error Handling#
1describe('UserService', () => {
2 describe('findById', () => {
3 it('throws NotFoundError when user does not exist', async () => {
4 await expect(userService.findById('non-existent'))
5 .rejects
6 .toThrow(NotFoundError);
7 });
8
9 it('returns user when found', async () => {
10 const user = await createTestUser();
11 const found = await userService.findById(user.id);
12 expect(found.id).toBe(user.id);
13 });
14 });
15
16 describe('create', () => {
17 it('throws ConflictError for duplicate email', async () => {
18 await createTestUser({ email: 'test@example.com' });
19
20 await expect(
21 userService.create({ email: 'test@example.com', password: 'Test123!' })
22 )
23 .rejects
24 .toThrow(ConflictError);
25 });
26
27 it('throws ValidationError for weak password', async () => {
28 await expect(
29 userService.create({ email: 'new@example.com', password: 'weak' })
30 )
31 .rejects
32 .toThrow(ValidationError);
33 });
34 });
35});
36
37// Test error handler
38describe('Error Handler', () => {
39 it('returns 404 for NotFoundError', async () => {
40 const response = await request(app)
41 .get('/users/non-existent')
42 .expect(404);
43
44 expect(response.body.error.code).toBe('NOT_FOUND');
45 });
46
47 it('returns 400 for validation errors', async () => {
48 const response = await request(app)
49 .post('/users')
50 .send({ email: 'invalid' })
51 .expect(400);
52
53 expect(response.body.error.code).toBe('VALIDATION_ERROR');
54 });
55});Best Practices#
Error Design:
✓ Use custom error classes
✓ Include error codes
✓ Separate operational from programmer errors
✓ Provide helpful messages
Handling:
✓ Always use try-catch with async/await
✓ Handle errors at appropriate level
✓ Clean up resources on failure
✓ Log errors with context
Response:
✓ Don't expose internal errors in production
✓ Use consistent error format
✓ Include request ID for debugging
✓ Return appropriate status codes
Monitoring:
✓ Log all errors
✓ Track error rates
✓ Alert on critical errors
✓ Review error logs regularly
Conclusion#
Robust error handling improves application reliability and debugging. Use custom error classes, centralized error handlers, and proper logging. Always catch async errors, handle cleanup on failure, and test error paths thoroughly.