Express Error Middleware
Error handling is a critical aspect of building robust web applications. In Express.js, error middleware provides a centralized way to catch and process errors throughout your application. Instead of handling errors in each route handler, you can delegate error handling to specialized middleware functions.
What is Error Middleware?
Error middleware in Express is a special type of middleware that has four parameters instead of the usual three:
function errorHandler(err, req, res, next) {
// Error handling logic goes here
}
The first parameter err
represents the error object that was passed to the next()
function. This distinction is how Express identifies an error-handling middleware function.
How Error Middleware Works
When an error occurs in an Express application, you can pass it to the next()
function:
app.get('/example', (req, res, next) => {
try {
// Some operation that might fail
if (someCondition) {
throw new Error('Something went wrong!');
}
res.send('Success!');
} catch (err) {
next(err); // Pass error to Express
}
});
Once an error is passed to next()
, Express will:
- Skip all remaining non-error handling middleware
- Execute any error middleware functions that are defined
Creating and Using Error Middleware
Basic Error Handler
Here's how to create and use a basic error handler:
const express = require('express');
const app = express();
// Regular middleware and routes
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.get('/error', (req, res, next) => {
// Simulating an error
const err = new Error('This is a test error');
next(err);
});
// Error middleware (must be defined AFTER all other routes and middleware)
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');
});
When you visit /error
, the application will display "Something broke!" instead of crashing.
Custom Error Response
You can customize the error response based on the error type or other conditions:
app.use((err, req, res, next) => {
// Set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// Set status code
const statusCode = err.statusCode || 500;
// Send response based on the requested format
res.status(statusCode);
if (req.accepts('html')) {
res.render('error', { error: err });
} else if (req.accepts('json')) {
res.json({ error: err.message });
} else {
res.type('txt').send(err.message);
}
});
Multiple Error Handlers
You can define multiple error handlers for different types of errors:
// Handle 404 errors
app.use((req, res, next) => {
const err = new Error('Not Found');
err.statusCode = 404;
next(err);
});
// Handle database errors
app.use((err, req, res, next) => {
if (err.name === 'MongoError' || err.name === 'SequelizeError') {
console.error('Database error:', err);
return res.status(500).send('Database error occurred');
}
next(err); // Pass to the next error handler
});
// Handle validation errors
app.use((err, req, res, next) => {
if (err.name === 'ValidationError') {
return res.status(400).json({
message: 'Validation Error',
errors: err.errors
});
}
next(err);
});
// Generic error handler (catch-all)
app.use((err, req, res, next) => {
const statusCode = err.statusCode || 500;
res.status(statusCode);
res.json({
status: statusCode,
message: err.message,
stack: process.env.NODE_ENV === 'production' ? '🥞' : err.stack
});
});
Creating Custom Error Classes
For better organization, you can create custom error classes:
// custom-errors.js
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}
class NotFoundError extends AppError {
constructor(message = 'Resource not found') {
super(message, 404);
}
}
class ValidationError extends AppError {
constructor(message = 'Validation failed', errors = {}) {
super(message, 400);
this.errors = errors;
}
}
module.exports = {
AppError,
NotFoundError,
ValidationError
};
Then use them in your routes:
const { NotFoundError, ValidationError } = require('./custom-errors');
app.get('/users/:id', (req, res, next) => {
const user = findUser(req.params.id);
if (!user) {
return next(new NotFoundError(`User with id ${req.params.id} not found`));
}
res.json(user);
});
app.post('/users', (req, res, next) => {
const { name, email } = req.body;
const errors = {};
if (!name) errors.name = 'Name is required';
if (!email) errors.email = 'Email is required';
if (Object.keys(errors).length > 0) {
return next(new ValidationError('Invalid user data', errors));
}
// Create user...
res.status(201).json(newUser);
});
Async Error Handling
For asynchronous route handlers, you need to ensure errors are properly passed to Express:
// Without helper - errors in promises won't be caught by Express
app.get('/async-problem', async (req, res) => {
// If this throws, Express won't catch it!
const data = await fetchSomeData();
res.json(data);
});
// Manual solution - wrap in try/catch
app.get('/async-manual', async (req, res, next) => {
try {
const data = await fetchSomeData();
res.json(data);
} catch (err) {
next(err);
}
});
// Helper function to wrap async functions
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
// Using the helper
app.get('/async-clean', asyncHandler(async (req, res) => {
const data = await fetchSomeData();
res.json(data);
}));
There are also libraries like express-async-errors
that can automatically handle this for you.
Real-World Example: API Error Handling
Here's a complete example of how to implement error handling in a REST API:
const express = require('express');
const app = express();
// Body parser middleware
app.use(express.json());
// API routes
app.use('/api/users', require('./routes/users'));
app.use('/api/products', require('./routes/products'));
// 404 handler for API routes
app.use('/api', (req, res, next) => {
const error = new Error('API endpoint not found');
error.statusCode = 404;
next(error);
});
// Generic API error handler
app.use('/api', (err, req, res, next) => {
const statusCode = err.statusCode || 500;
// Log error details for server-side debugging
console.error(`[${new Date().toISOString()}] ${err.stack}`);
// Send appropriate response to client
res.status(statusCode).json({
status: 'error',
statusCode,
message: err.message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
...(err.errors && { errors: err.errors })
});
});
// Generic error handler for non-API routes
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.statusCode || 500);
res.render('error', {
message: err.message,
error: process.env.NODE_ENV === 'development' ? err : {}
});
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
Best Practices
- Define error middleware last - After all other
app.use()
and routes calls. - Use different error middleware for API vs. webpage responses - APIs should return JSON, web pages should return HTML.
- Log errors - Always log errors for debugging purposes, but be careful not to expose sensitive information to users.
- Use appropriate status codes - Don't just use 500 for every error.
- Handle async errors properly - Use try/catch or helper functions to ensure Express catches all errors.
- Include stack traces in development only - Never expose stack traces in production.
- Create custom error classes - For better error categorization and handling.
Summary
Error middleware in Express.js provides a powerful way to handle errors in your application:
- Error middleware has four parameters:
(err, req, res, next)
- You can have multiple error handlers for different types of errors
- Error middleware should be defined after all other middleware and routes
- Express skips all regular middleware when an error is passed to
next()
- Custom error classes can help organize and standardize error handling
By implementing robust error handling with Express middleware, you can create more resilient applications that provide better feedback to users and administrators when things go wrong.
Additional Resources
Exercises
- Create a simple Express app with a route that generates different types of errors based on query parameters.
- Implement multiple error handlers to deal with each type of error differently.
- Create a custom error class hierarchy for your application's specific needs.
- Implement an error logging system that logs different levels of detail based on the error type and environment.
- Create a middleware that transforms validation errors from a library like Joi or express-validator into a standardized format.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)