Express.js 에러 핸들링 미들웨어 패턴
Building robust error handling middleware in Express.js with centralized error logging, response formatting, and async error catching.
Express.js 에러 핸들링 미들웨어 패턴
Introduction
Proper error handling is the difference between a production-ready application and one that crashes on unexpected input. Express.js error handling middleware provides a centralized way to catch, log, and respond to errors consistently. This post covers building a complete error handling system.
Environment
node --version
# v20.11.0
npm list express
# my-app@1.0.0
# +-- express@4.18.2Problem
Without centralized error handling, error handling is scattered throughout the codebase:
app.get('/users/:id', async (req, res) => {
try {
const user = await db.users.findById(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Internal server error' });
}
});
app.post('/users', async (req, res) => {
try {
const user = await db.users.create(req.body);
res.status(201).json(user);
} catch (err) {
console.error(err);
// Duplicate error handling code!
res.status(500).json({ error: 'Internal server error' });
}
});This approach has several problems:
- Duplicate error handling code in every route
- Inconsistent error response formats
- Errors in middleware are not caught
- Async errors may crash the process
Analysis
Express error handling middleware follows a specific signature:
// Error middleware MUST have exactly 4 parameters
app.use((err, req, res, next) => {
// This catches errors passed via next(err)
});The error flow in Express:
Route throws error → next(err) → Error middleware → ResponseFor async functions, errors must be caught and passed to next():
// This does NOT catch async errors!
app.get('/users', async (req, res) => {
const users = await db.users.findAll(); // If this throws, server crashes
res.json(users);
});Solution
Solution 1: Custom error classes
// errors/AppError.js
class AppError extends Error {
constructor(message, statusCode, code) {
super(message);
this.statusCode = statusCode;
this.code = code || 'INTERNAL_ERROR';
this.isOperational = true;
}
}
class NotFoundError extends AppError {
constructor(resource = 'Resource') {
super(`${resource} not found`, 404, 'NOT_FOUND');
}
}
class ValidationError extends AppError {
constructor(errors) {
super('Validation failed', 400, 'VALIDATION_ERROR');
this.errors = errors;
}
}
class AuthenticationError extends AppError {
constructor(message = 'Authentication required') {
super(message, 401, 'AUTHENTICATION_ERROR');
}
}
module.exports = { AppError, NotFoundError, ValidationError, AuthenticationError };Solution 2: Async error wrapper
// middleware/asyncHandler.js
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
module.exports = asyncHandler;
// Usage
const asyncHandler = require('./middleware/asyncHandler');
app.get('/users/:id', asyncHandler(async (req, res) => {
const user = await db.users.findById(req.params.id);
if (!user) {
throw new NotFoundError('User');
}
res.json(user);
}));Solution 3: Centralized error handler
// middleware/errorHandler.js
const { AppError } = require('../errors/AppError');
const errorHandler = (err, req, res, next) => {
// Log error
console.error('Error:', {
message: err.message,
stack: err.stack,
path: req.path,
method: req.method,
timestamp: new Date().toISOString()
});
// Handle known operational errors
if (err instanceof AppError) {
return res.status(err.statusCode).json({
error: {
code: err.code,
message: err.message,
...(err.errors && { details: err.errors })
}
});
}
// Handle validation errors
if (err.name === 'ValidationError') {
return res.status(400).json({
error: {
code: 'VALIDATION_ERROR',
message: err.message,
details: err.errors
}
});
}
// Handle JWT errors
if (err.name === 'JsonWebTokenError') {
return res.status(401).json({
error: {
code: 'INVALID_TOKEN',
message: 'Invalid token'
}
});
}
// Handle unknown errors
res.status(500).json({
error: {
code: 'INTERNAL_ERROR',
message: process.env.NODE_ENV === 'production'
? 'An unexpected error occurred'
: err.message
}
});
};
module.exports = errorHandler;
// server.js
const express = require('express');
const errorHandler = require('./middleware/errorHandler');
const app = express();
// ... routes ...
// Error handler MUST be last middleware
app.use(errorHandler);Solution 4: 404 handler for unmatched routes
// middleware/notFound.js
const notFoundHandler = (req, res) => {
res.status(404).json({
error: {
code: 'NOT_FOUND',
message: `Route ${req.method} ${req.path} not found`
}
});
};
module.exports = notFoundHandler;
// server.js
app.use('/api', apiRoutes);
app.use(notFoundHandler); // After all routes
app.use(errorHandler); // After 404 handlerSolution 5: Complete application setup
// server.js
const express = require('express');
const asyncHandler = require('./middleware/asyncHandler');
const errorHandler = require('./middleware/errorHandler');
const notFoundHandler = require('./middleware/notFound');
const { NotFoundError, ValidationError } = require('./errors/AppError');
const app = express();
app.use(express.json());
// Health check (no error handling needed)
app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
// API routes with asyncHandler
app.get('/api/users/:id', asyncHandler(async (req, res) => {
const user = await db.users.findById(req.params.id);
if (!user) throw new NotFoundError('User');
res.json(user);
}));
app.post('/api/users', asyncHandler(async (req, res) => {
const { email, name } = req.body;
if (!email || !name) {
throw new ValidationError(['Email and name are required']);
}
const user = await db.users.create({ email, name });
res.status(201).json(user);
}));
// Error handling chain
app.use(notFoundHandler);
app.use(errorHandler);
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});Lessons Learned
- Always use async/await with asyncHandler - Never forget error handling for async routes
- Define error classes for different error types - It makes error handling more precise
- Centralize error logging - One place to log all errors consistently
- Hide error details in production - Do not leak stack traces to clients
- Test error paths - Errors should be tested as thoroughly as happy paths
This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.