Error Handling
Implement proper error handling with custom error classes and middleware
Error Handling in Express
Proper error handling is crucial for building robust APIs. Express has built-in error handling, but you'll want to customize it for production applications.
⚠️ Error Handler Signature
// Error middleware has 4 parameters (err, req, res, next)
// Express recognizes this signature as an error handler
app.use((err, req, res, next) => {
// Handle error
});
Basic Error Handling
import express, { Request, Response, NextFunction } from 'express';
const app = express();
// Route that throws an error
app.get('/error', (req, res) => {
throw new Error('Something went wrong!');
});
// Async route with error
app.get('/async-error', async (req, res, next) => {
try {
const data = await someAsyncOperation();
res.json(data);
} catch (error) {
next(error); // Pass to error handler
}
});
// Error handling middleware (MUST be last)
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
console.error(err.stack);
res.status(500).json({
error: 'Internal Server Error',
message: err.message
});
});
Custom Error Classes
// errors/AppError.ts
export class AppError extends Error {
statusCode: number;
isOperational: boolean;
constructor(message: string, statusCode: number) {
super(message);
this.statusCode = statusCode;
this.isOperational = true; // Trusted, expected errors
Error.captureStackTrace(this, this.constructor);
}
}
// Common error types
export class NotFoundError extends AppError {
constructor(resource: string = 'Resource') {
super(`${resource} not found`, 404);
}
}
export class BadRequestError extends AppError {
constructor(message: string = 'Bad request') {
super(message, 400);
}
}
export class UnauthorizedError extends AppError {
constructor(message: string = 'Unauthorized') {
super(message, 401);
}
}
export class ForbiddenError extends AppError {
constructor(message: string = 'Forbidden') {
super(message, 403);
}
}
export class ValidationError extends AppError {
errors: any[];
constructor(errors: any[]) {
super('Validation failed', 400);
this.errors = errors;
}
}
Error Handler Middleware
// middleware/errorHandler.ts
import { Request, Response, NextFunction } from 'express';
import { AppError, ValidationError } from '../errors/AppError';
interface ErrorResponse {
status: 'error' | 'fail';
message: string;
errors?: any[];
stack?: string;
}
export const errorHandler = (
err: Error,
req: Request,
res: Response,
next: NextFunction
) => {
// Log error
console.error(`[${new Date().toISOString()}] Error:`, {
message: err.message,
stack: err.stack,
path: req.path,
method: req.method
});
// Default values
let statusCode = 500;
let response: ErrorResponse = {
status: 'error',
message: 'Internal Server Error'
};
// Handle known errors
if (err instanceof AppError) {
statusCode = err.statusCode;
response = {
status: statusCode < 500 ? 'fail' : 'error',
message: err.message
};
if (err instanceof ValidationError) {
response.errors = err.errors;
}
}
// Handle specific error types
if (err.name === 'JsonWebTokenError') {
statusCode = 401;
response.message = 'Invalid token';
}
if (err.name === 'TokenExpiredError') {
statusCode = 401;
response.message = 'Token expired';
}
// Include stack in development
if (process.env.NODE_ENV === 'development') {
response.stack = err.stack;
}
res.status(statusCode).json(response);
};
// 404 handler
export const notFoundHandler = (req: Request, res: Response) => {
res.status(404).json({
status: 'fail',
message: `Route ${req.method} ${req.path} not found`
});
};
Async Error Wrapper
// utils/asyncHandler.ts
import { Request, Response, NextFunction } from 'express';
type AsyncHandler = (
req: Request,
res: Response,
next: NextFunction
) => Promise;
// Wrap async functions to catch errors automatically
export const asyncHandler = (fn: AsyncHandler) => {
return (req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
// Usage - no try/catch needed!
import { asyncHandler } from './utils/asyncHandler';
import { NotFoundError } from './errors/AppError';
app.get('/users/:id', asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) {
throw new NotFoundError('User');
}
res.json(user);
}));
// Alternative: express-async-errors package
// Just import it once at the top of your app
import 'express-async-errors';
// Now async errors are caught automatically
app.get('/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) throw new NotFoundError('User');
res.json(user);
});
Complete Error Handling Setup
// app.ts
import express from 'express';
import 'express-async-errors'; // Auto-catch async errors
import { errorHandler, notFoundHandler } from './middleware/errorHandler';
import { NotFoundError, BadRequestError } from './errors/AppError';
import userRoutes from './routes/users';
const app = express();
app.use(express.json());
// Routes
app.use('/api/users', userRoutes);
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'healthy' });
});
// 404 handler for unmatched routes
app.use(notFoundHandler);
// Global error handler (MUST BE LAST)
app.use(errorHandler);
export default app;
// routes/users.ts
import { Router } from 'express';
import { NotFoundError, BadRequestError } from '../errors/AppError';
const router = Router();
router.get('/:id', async (req, res) => {
const { id } = req.params;
if (!isValidId(id)) {
throw new BadRequestError('Invalid user ID format');
}
const user = await User.findById(id);
if (!user) {
throw new NotFoundError('User');
}
res.json(user);
});
router.post('/', async (req, res) => {
const { email } = req.body;
const existing = await User.findByEmail(email);
if (existing) {
throw new BadRequestError('Email already in use');
}
const user = await User.create(req.body);
res.status(201).json(user);
});
export default router;
Production Error Handling
// Unhandled rejection handler
process.on('unhandledRejection', (reason: Error) => {
console.error('Unhandled Rejection:', reason);
// Log to error tracking service
// Sentry.captureException(reason);
});
// Uncaught exception handler
process.on('uncaughtException', (error: Error) => {
console.error('Uncaught Exception:', error);
// Log to error tracking service
// Sentry.captureException(error);
// Graceful shutdown
process.exit(1);
});
// Graceful shutdown
const server = app.listen(PORT);
process.on('SIGTERM', () => {
console.log('SIGTERM received. Shutting down gracefully...');
server.close(() => {
console.log('Process terminated.');
process.exit(0);
});
});
✅ Error Handling Best Practices
- • Create custom error classes for different error types
- • Use async wrapper or express-async-errors
- • Error handler middleware must be last
- • Don't expose stack traces in production
- • Log errors with context (path, method, user)
- • Use error tracking (Sentry, LogRocket, etc.)
- • Handle unhandledRejection and uncaughtException