Skip to main content

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:

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

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

  1. Skip all remaining non-error handling middleware
  2. 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:

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

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

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

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

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

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

javascript
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

  1. Define error middleware last - After all other app.use() and routes calls.
  2. Use different error middleware for API vs. webpage responses - APIs should return JSON, web pages should return HTML.
  3. Log errors - Always log errors for debugging purposes, but be careful not to expose sensitive information to users.
  4. Use appropriate status codes - Don't just use 500 for every error.
  5. Handle async errors properly - Use try/catch or helper functions to ensure Express catches all errors.
  6. Include stack traces in development only - Never expose stack traces in production.
  7. 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

  1. Create a simple Express app with a route that generates different types of errors based on query parameters.
  2. Implement multiple error handlers to deal with each type of error differently.
  3. Create a custom error class hierarchy for your application's specific needs.
  4. Implement an error logging system that logs different levels of detail based on the error type and environment.
  5. 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! :)