Skip to main content

Express Error Handling

When building web applications with Express.js, errors are inevitable. Whether it's invalid user input, failed database connections, or unexpected server issues, proper error handling is crucial for maintaining a stable and user-friendly application. In this guide, we'll explore how to effectively handle errors in Express applications.

Why Error Handling Matters

Proper error handling provides several benefits:

  • Better User Experience: Users receive meaningful error messages instead of cryptic failures
  • Improved Debugging: Developers can quickly identify and fix issues
  • Application Stability: Prevents crashes by catching and handling exceptions
  • Security: Avoids revealing sensitive information in error messages

Express Default Error Handling

Express comes with a built-in error handler that takes care of any errors that might occur in your application. If you don't handle errors explicitly, Express will catch them and send a basic response.

However, the default error handler is minimal and typically not sufficient for production applications. Let's see how it works:

javascript
const express = require('express');
const app = express();

app.get('/example', (req, res) => {
// This will trigger the default Express error handler
throw new Error('Something went wrong!');
});

app.listen(3000, () => {
console.log('Server running on port 3000');
});

If you navigate to /example in your browser, Express will respond with a 500 status code and a basic error message. In development mode, it includes the stack trace, but in production, it shows minimal information.

Using Try-Catch for Error Handling

A simple approach for handling errors in route handlers is using try-catch blocks:

javascript
app.get('/users/:id', async (req, res) => {
try {
const user = await findUserById(req.params.id);

if (!user) {
return res.status(404).json({ error: 'User not found' });
}

res.json(user);
} catch (error) {
console.error('Error fetching user:', error);
res.status(500).json({ error: 'Internal server error' });
}
});

This approach works, but can lead to repetitive code across multiple route handlers. Let's look at a more efficient approach.

Custom Error Handling Middleware

Express allows you to create custom error-handling middleware functions. These are similar to regular middleware but take four arguments instead of three: (err, req, res, next).

Here's how to create and use a custom error handler:

javascript
const express = require('express');
const app = express();

// Regular route handlers
app.get('/users/:id', async (req, res, next) => {
try {
const userId = req.params.id;
// Simulate database error for certain ID
if (userId === '999') {
throw new Error('Database connection failed');
}

// Simulate user not found
if (userId === '404') {
const error = new Error('User not found');
error.statusCode = 404;
throw error;
}

// Success case
res.json({ id: userId, name: 'Example User' });
} catch (error) {
// Pass error to error handling middleware
next(error);
}
});

// 404 handler for undefined routes
app.use((req, res, next) => {
const error = new Error('Not Found');
error.statusCode = 404;
next(error);
});

// Error handling middleware
app.use((err, req, res, next) => {
const statusCode = err.statusCode || 500;

// Log error for debugging (but don't expose sensitive info)
console.error(`${statusCode} - ${err.message}`);

res.status(statusCode).json({
status: 'error',
statusCode,
message: statusCode === 500 ? 'Internal Server Error' : err.message
});
});

app.listen(3000, () => {
console.log('Server running on port 3000');
});

Example Results:

  1. Accessing /users/123:

    json
    {
    "id": "123",
    "name": "Example User"
    }
  2. Accessing /users/404:

    json
    {
    "status": "error",
    "statusCode": 404,
    "message": "User not found"
    }
  3. Accessing /users/999:

    json
    {
    "status": "error",
    "statusCode": 500,
    "message": "Internal Server Error"
    }
  4. Accessing any undefined route:

    json
    {
    "status": "error",
    "statusCode": 404,
    "message": "Not Found"
    }

Creating Custom Error Classes

For more structured error handling, you can create custom error classes:

javascript
// errorTypes.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') {
super(message, 400);
}
}

class DatabaseError extends AppError {
constructor(message = 'Database error occurred') {
super(message, 500);
}
}

module.exports = {
AppError,
NotFoundError,
ValidationError,
DatabaseError
};

Now we can use these custom error classes in our routes:

javascript
const express = require('express');
const { NotFoundError, ValidationError } = require('./errorTypes');
const app = express();

app.use(express.json());

app.post('/users', (req, res, next) => {
try {
const { username, email } = req.body;

// Validation check
if (!username || !email) {
throw new ValidationError('Username and email are required');
}

// Success case - in a real app, you would save to database
res.status(201).json({
message: 'User created successfully',
user: { username, email }
});
} catch (error) {
next(error);
}
});

// Error middleware remains the same as previous example
app.use((err, req, res, next) => {
const statusCode = err.statusCode || 500;

console.error(`${statusCode} - ${err.message}`);

res.status(statusCode).json({
status: 'error',
statusCode,
message: err.name === 'DatabaseError' || statusCode === 500
? 'Internal Server Error'
: err.message
});
});

app.listen(3000);

Async Error Handling

When working with async/await, you have two options:

1. Try-Catch with Next

javascript
app.get('/async-example', async (req, res, next) => {
try {
const data = await fetchDataFromDatabase();
res.json(data);
} catch (error) {
next(error);
}
});

2. Using Express-Async-Errors Package

The express-async-errors package automatically catches errors in async functions and forwards them to your error handlers. This eliminates the need for try-catch blocks:

First, install the package:

bash
npm install express-async-errors

Then use it in your application:

javascript
const express = require('express');
// This import must come before any route definitions
require('express-async-errors');

const app = express();

// No try-catch needed! Errors are automatically forwarded to error handlers
app.get('/users/:id', async (req, res) => {
const user = await findUserById(req.params.id);

if (!user) {
throw new Error('User not found');
}

res.json(user);
});

// Error handler still needed
app.use((err, req, res, next) => {
// Handle errors here
res.status(500).json({ error: err.message });
});

app.listen(3000);

Real-World Example: REST API with Error Handling

Let's build a more complete example of a REST API with proper error handling:

javascript
const express = require('express');
require('express-async-errors');
const app = express();

// Custom error classes
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.name = this.constructor.name;
}
}

// Middleware
app.use(express.json());

// Request logging middleware
app.use((req, res, next) => {
console.log(`${req.method} ${req.path}`);
next();
});

// Mock database
const users = [
{ id: '1', name: 'Alice', email: '[email protected]' },
{ id: '2', name: 'Bob', email: '[email protected]' }
];

// Routes
app.get('/users', (req, res) => {
res.json(users);
});

app.get('/users/:id', (req, res) => {
const user = users.find(u => u.id === req.params.id);

if (!user) {
throw new AppError('User not found', 404);
}

res.json(user);
});

app.post('/users', (req, res) => {
const { name, email } = req.body;

if (!name || !email) {
throw new AppError('Name and email are required', 400);
}

// In a real app, validate email format
if (!email.includes('@')) {
throw new AppError('Invalid email format', 400);
}

const newUser = {
id: (users.length + 1).toString(),
name,
email
};

users.push(newUser);
res.status(201).json(newUser);
});

// 404 handler
app.use((req, res, next) => {
next(new AppError(`Route ${req.originalUrl} not found`, 404));
});

// Error handling middleware
app.use((err, req, res, next) => {
console.error(`Error: ${err.name} - ${err.message}`);

const statusCode = err.statusCode || 500;
const message = statusCode === 500 ? 'Internal server error' : err.message;

res.status(statusCode).json({
status: 'error',
statusCode,
message,
// Include stack trace only in development
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
});

// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});

Best Practices for Error Handling

  1. Always use error handling middleware: Centralize your error handling logic
  2. Hide sensitive information: Never expose stack traces or sensitive details in production
  3. Log errors properly: Use a logging library like Winston or Pino for better error tracking
  4. Use custom error classes: Create a hierarchy of error types for better organization
  5. Set appropriate status codes: Use standard HTTP status codes to indicate error types
  6. Handle async errors properly: Either use try/catch with next() or the express-async-errors package
  7. Validate input: Catch validation errors early using middleware like express-validator
  8. Have fallback error handlers: Always catch unexpected errors to prevent app crashes

Environment-Specific Error Handling

In development, you want detailed error information. In production, you want to hide implementation details:

javascript
// Error handling middleware with environment awareness
app.use((err, req, res, next) => {
// Always log errors
console.error(err);

// Set appropriate status code
const statusCode = err.statusCode || 500;

// Prepare response
const errorResponse = {
status: 'error',
statusCode,
message: statusCode === 500 ? 'Internal server error' : err.message,
};

// Add stack trace in development mode only
if (process.env.NODE_ENV === 'development') {
errorResponse.stack = err.stack;
errorResponse.details = err.details || {};
}

res.status(statusCode).json(errorResponse);
});

Summary

Effective error handling is essential for building robust Express applications. In this guide, we've covered:

  • Default Express error handling
  • Using try-catch blocks for basic error handling
  • Creating custom error handling middleware
  • Building custom error classes for different error types
  • Working with async functions and error handling
  • Environment-specific error handling for development and production
  • Best practices for managing errors

By implementing these patterns, your Express applications will be more stable, easier to debug, and provide a better experience for your users.

Additional Resources

Exercises

  1. Create a simple Express application with custom error classes for at least three different error types.
  2. Implement a middleware that validates user input and throws appropriate errors.
  3. Build a REST API with endpoints that might fail, and add comprehensive error handling.
  4. Modify the error handling middleware to log errors to a file instead of console.
  5. Create a system that sends different error responses based on content negotiation (HTML for browsers, JSON for API clients).


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)