Express Error Middleware
Introduction
When building web applications with Express.js, handling errors effectively is crucial for creating robust and user-friendly experiences. Express Error Middleware provides a centralized mechanism to catch and process errors that occur during request handling, allowing you to respond appropriately without crashing your application.
In this guide, we'll explore how Express Error Middleware works, how to create custom error handlers, and best practices for implementing error handling in your Express applications.
Understanding Express Error Middleware
In Express, middleware functions are functions that have access to the request object (req
), the response object (res
), and the next middleware function in the application's request-response cycle. Error middleware functions are similar but include an additional parameter at the beginning: the error object.
The Signature of Error Middleware
function errorMiddleware(err, req, res, next) {
// Handle the error
}
The key distinguishing feature of error middleware is the presence of four parameters instead of the usual three. Even if you don't use the next
parameter in your error handler, you must include it in the function signature so that Express recognizes it as an error-handling middleware.
How Error Middleware Works
Express error middleware operates based on a few key principles:
- Express identifies error middleware by the presence of four parameters in the function signature
- Error middleware is defined after all other route handlers and middleware
- When an error occurs, Express skips all remaining standard middleware and routes, moving directly to error middleware
- Multiple error middleware functions can be chained, similar to regular middleware
Creating Basic Error Middleware
Here's a simple example of how to define and use error middleware in an Express application:
const express = require('express');
const app = express();
// Regular route handler
app.get('/', (req, res) => {
res.send('Hello World!');
});
// Route that throws an error
app.get('/error', (req, res) => {
throw new Error('Something went wrong!');
});
// Error middleware (notice the 4 parameters)
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Something broke!');
});
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
In this example, when a user visits /error
, the route handler throws an error. Express catches this error and passes it to our error middleware, which logs the error stack and sends a 500 response to the client.
Handling Asynchronous Errors
Express doesn't automatically catch errors in asynchronous code. For async functions or promises, you need to either use try/catch
with next(err)
or use Express 4.16+ with async route handlers:
Using try/catch
app.get('/async-error', async (req, res, next) => {
try {
// Simulating async operation that throws
const result = await someAsyncFunction();
res.send(result);
} catch (err) {
next(err); // Pass error to Express
}
});
Using Express 4.16+ async handlers
With Express 4.16 and above, you can directly use async functions and Express will catch rejected promises:
app.get('/async-error', async (req, res) => {
// If this rejects, Express will catch it automatically
const result = await someAsyncFunction();
res.send(result);
});
Creating Custom Error Handlers
You can create custom error types and middleware to handle different kinds of errors differently:
// Custom error types
class NotFoundError extends Error {
constructor(message) {
super(message);
this.name = 'NotFoundError';
this.statusCode = 404;
}
}
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = 'ValidationError';
this.statusCode = 400;
}
}
// Route that throws custom error
app.get('/users/:id', (req, res, next) => {
const userId = req.params.id;
if (!userId) {
return next(new ValidationError('User ID is required'));
}
// Simulate user not found
if (userId === '999') {
return next(new NotFoundError('User not found'));
}
res.send({ id: userId, name: 'Sample User' });
});
// Error middleware that handles different error types
app.use((err, req, res, next) => {
console.error(err);
// Set default status code
const statusCode = err.statusCode || 500;
// Structure the error response
const errorResponse = {
error: {
message: err.message || 'Internal Server Error',
type: err.name || 'Error'
}
};
// In development, include stack trace
if (process.env.NODE_ENV === 'development') {
errorResponse.error.stack = err.stack;
}
res.status(statusCode).json(errorResponse);
});
Multiple Error Middleware Functions
You can have multiple error middleware functions for different purposes:
// Log all errors
app.use((err, req, res, next) => {
console.error(`[${new Date().toISOString()}] ${err}`);
next(err); // Pass error to next handler
});
// Handle 404 errors
app.use((err, req, res, next) => {
if (err.name === 'NotFoundError') {
return res.status(404).json({
error: {
message: err.message,
type: 'NotFound'
}
});
}
next(err); // Pass to next error handler
});
// Handle other errors
app.use((err, req, res, next) => {
const statusCode = err.statusCode || 500;
res.status(statusCode).json({
error: {
message: err.message || 'Internal Server Error',
type: err.name || 'Error'
}
});
});
Real-world Example: API Error Handler
Here's a more comprehensive example showing how to create an API error handler with proper error codes and responses:
const express = require('express');
const app = express();
// Base error class
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true; // Indicates if this is an expected error
Error.captureStackTrace(this, this.constructor);
}
}
// API routes
app.get('/api/products', (req, res, next) => {
try {
// Simulating database operation
const products = [
{ id: 1, name: 'Laptop' },
{ id: 2, name: 'Phone' }
];
res.json(products);
} catch (err) {
next(new AppError('Failed to fetch products', 500));
}
});
app.get('/api/products/:id', (req, res, next) => {
try {
const productId = parseInt(req.params.id);
// Validate input
if (isNaN(productId)) {
return next(new AppError('Invalid product ID', 400));
}
// Simulate product lookup
const product = productId === 1 ?
{ id: 1, name: 'Laptop' } : null;
if (!product) {
return next(new AppError('Product not found', 404));
}
res.json(product);
} catch (err) {
next(err);
}
});
// Error logger middleware
app.use((err, req, res, next) => {
// Log error details for debugging
console.error(`
Error: ${err.message}
Path: ${req.path}
Method: ${req.method}
Time: ${new Date().toISOString()}
${err.stack}
`);
next(err);
});
// API error response formatter
app.use((err, req, res, next) => {
const statusCode = err.statusCode || 500;
// Format the error response
const errorResponse = {
status: 'error',
message: err.message || 'Something went wrong',
code: err.statusCode || 500,
path: req.path
};
// Add stack trace in development environment
if (process.env.NODE_ENV === 'development' && err.stack) {
errorResponse.stack = err.stack;
}
// Send JSON for API errors
if (req.path.startsWith('/api')) {
return res.status(statusCode).json(errorResponse);
}
// Render error page for website
res.status(statusCode).render('error', {
title: 'Error',
message: err.message || 'Something went wrong'
});
});
// Handle 404 for routes not found
app.use((req, res) => {
if (req.path.startsWith('/api')) {
return res.status(404).json({
status: 'error',
message: 'Endpoint not found',
code: 404,
path: req.path
});
}
res.status(404).render('404', { title: 'Page Not Found' });
});
app.listen(3000, () => {
console.log('Server started on port 3000');
});
In this example:
- We define a custom
AppError
class that extends the built-inError
class - We have error handling for API routes, with proper status codes and error messages
- We use separate error middleware for logging and formatting responses
- We handle API errors differently from website errors
Best Practices for Error Middleware
-
Create a central error handler: Consolidate your error handling logic in one place.
-
Use appropriate HTTP status codes: Return the correct HTTP status code based on the error type.
-
Provide meaningful error messages: Give users enough information to understand what went wrong.
-
Avoid exposing sensitive information: Don't include stack traces or database errors in production.
-
Log errors properly: Include enough context for debugging without logging sensitive data.
-
Differentiate between operational errors and programming errors:
- Operational errors are expected (e.g., user not found)
- Programming errors are bugs that should be fixed
-
Handle uncaught exceptions and unhandled promise rejections:
// Last-resort error handlers
process.on('uncaughtException', (err) => {
console.error('UNCAUGHT EXCEPTION! Shutting down...');
console.error(err.name, err.message, err.stack);
process.exit(1);
});
process.on('unhandledRejection', (err) => {
console.error('UNHANDLED REJECTION! Shutting down...');
console.error(err.name, err.message, err.stack);
process.exit(1);
});
Common Error Patterns
Validation Error
app.post('/api/users', (req, res, next) => {
const { name, email } = req.body;
if (!name) {
return next(new AppError('Name is required', 400));
}
if (!email || !email.includes('@')) {
return next(new AppError('Valid email is required', 400));
}
// Process valid data
res.status(201).json({
status: 'success',
data: { name, email }
});
});
Not Found Error
app.get('/api/users/:id', async (req, res, next) => {
try {
const user = await findUserById(req.params.id);
if (!user) {
return next(new AppError('User not found', 404));
}
res.json({
status: 'success',
data: { user }
});
} catch (err) {
next(new AppError('Error retrieving user', 500));
}
});
Summary
Express Error Middleware provides a powerful mechanism for handling errors in your Express applications. By implementing proper error handling, you can:
- Catch and process errors in a centralized location
- Provide appropriate responses based on error types
- Avoid application crashes from unhandled errors
- Improve user experience with clear error messages
- Debug more efficiently with detailed error logs
Remember, effective error handling is a key component of any production-ready application. By implementing the patterns and practices covered in this guide, you'll create more robust, maintainable Express applications.
Additional Resources
Practice Exercises
-
Create a middleware that handles different types of database errors and translates them into user-friendly messages.
-
Implement an error handler that formats errors differently based on the client's preferred format (JSON, XML, HTML).
-
Build a validation middleware using a library like Joi or express-validator, and integrate it with your error handling system.
-
Create a rate-limiting middleware that throws custom errors when a user exceeds their allowed requests.
-
Implement a system that logs errors to a file or external service, with different logging levels based on error severity.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)