Skip to main content

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

javascript
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:

  1. Express identifies error middleware by the presence of four parameters in the function signature
  2. Error middleware is defined after all other route handlers and middleware
  3. When an error occurs, Express skips all remaining standard middleware and routes, moving directly to error middleware
  4. 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:

javascript
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

javascript
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:

javascript
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:

javascript
// 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:

javascript
// 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:

javascript
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-in Error 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

  1. Create a central error handler: Consolidate your error handling logic in one place.

  2. Use appropriate HTTP status codes: Return the correct HTTP status code based on the error type.

  3. Provide meaningful error messages: Give users enough information to understand what went wrong.

  4. Avoid exposing sensitive information: Don't include stack traces or database errors in production.

  5. Log errors properly: Include enough context for debugging without logging sensitive data.

  6. Differentiate between operational errors and programming errors:

    • Operational errors are expected (e.g., user not found)
    • Programming errors are bugs that should be fixed
  7. Handle uncaught exceptions and unhandled promise rejections:

javascript
// 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

javascript
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

javascript
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

  1. Create a middleware that handles different types of database errors and translates them into user-friendly messages.

  2. Implement an error handler that formats errors differently based on the client's preferred format (JSON, XML, HTML).

  3. Build a validation middleware using a library like Joi or express-validator, and integrate it with your error handling system.

  4. Create a rate-limiting middleware that throws custom errors when a user exceeds their allowed requests.

  5. 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! :)