Error Handling Middleware in Express.js

Building robust APIs with effective error management

Understanding Errors in Express Applications

Error handling is one of the most critical aspects of creating robust web applications. In production environments, proper error handling can mean the difference between a minor hiccup and a complete service outage.

Why Error Handling Matters

flowchart TD A[Error Sources in Express Apps] --> B[Synchronous Code Errors] A --> C[Asynchronous Code Errors] A --> D[Database Errors] A --> E[Third-party API Errors] A --> F[Validation Errors] A --> G[Authentication Errors] A --> H[File System Errors] B --> B1[Syntax errors] B --> B2[Type errors] B --> B3[Reference errors] C --> C1[Unhandled Promise rejections] C --> C2[Callback errors] C --> C3[Event emitter errors] D --> D1[Connection failures] D --> D2[Query errors] D --> D3[Constraint violations] E --> E1[Timeouts] E --> E2[Rate limiting] E --> E3[API changes]

Think of error handling as a safety net for your application. Just as a circus performer needs a safety net to catch them if they fall, your code needs error handling to prevent it from crashing when unexpected situations occur.

Express Error Handling Fundamentals

Express provides a built-in error handling system, centered around special middleware functions that take four parameters instead of the usual three.

Basic Express Error Handler

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

// Regular middleware and routes
app.get('/api/items', (req, res) => {
  // Route handler code...
});

// Error handling middleware (note the four parameters)
app.use((err, req, res, next) => {
  console.error(err.stack);
  
  // Send error response
  res.status(500).json({
    error: {
      message: 'Something went wrong!'
    }
  });
});

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

Key Points About Error Middleware

  • Error middleware must have exactly four parameters: (err, req, res, next)
  • Express identifies error-handling middleware by the number of parameters
  • Error handlers should be defined after all other middleware and routes
  • Multiple error handlers can be chained to handle different types of errors
  • The next() function can be used to pass control to the next error handler
Express Middleware Execution Flow with Errors Client Request Express Application Middleware 1 Middleware 2 Route Handler Error thrown! Middleware 3 Middleware 4 Error Handler Error Response

Error Types and Classification

To handle errors effectively, it's important to understand the different types of errors that can occur in your application.

Operational vs. Programmer Errors

A useful way to classify errors is to divide them into operational errors and programmer errors:

Common Error Types in Node.js

// Operational Errors - Can be handled gracefully
try {
  // Database connection failure
  await db.connect();
} catch (err) {
  if (err.code === 'ECONNREFUSED') {
    console.error('Database connection failed, retrying...');
    // Retry logic...
  }
}

// Validation error - Expected in normal operation
if (!isValidEmail(email)) {
  const validationError = new Error('Invalid email format');
  validationError.statusCode = 400;
  validationError.type = 'VALIDATION_ERROR';
  throw validationError;
}

// Programmer Errors - Should be fixed, not handled
function processData(data) {
  // Trying to access property of undefined (programmer error)
  return data.items.map(item => item.name); // Will crash if data or data.items is undefined
}

// Better approach to prevent programmer error
function processData(data) {
  // Check assumptions before proceeding
  if (!data || !data.items || !Array.isArray(data.items)) {
    throw new TypeError('Invalid data format');
  }
  return data.items.map(item => item.name);
}

JavaScript Built-in Error Types

JavaScript provides several built-in error types that provide useful information about what went wrong:

Creating Custom Error Classes

For more advanced error handling, you'll want to create custom error classes that extend the base Error class. This allows you to add additional properties and methods to your errors.

Creating Custom Error Classes

// Base application error class
class AppError extends Error {
  constructor(message, statusCode = 500, type = 'SERVER_ERROR') {
    super(message);
    this.statusCode = statusCode;
    this.type = type;
    this.isOperational = true; // Flag for operational vs programmer errors
    
    // Capture stack trace, excluding the constructor call from it
    Error.captureStackTrace(this, this.constructor);
  }
}

// Specific error types
class ValidationError extends AppError {
  constructor(message, invalidFields = []) {
    super(message, 400, 'VALIDATION_ERROR');
    this.invalidFields = invalidFields;
  }
}

class NotFoundError extends AppError {
  constructor(resource = 'Resource') {
    super(`${resource} not found`, 404, 'NOT_FOUND');
    this.resource = resource;
  }
}

class AuthenticationError extends AppError {
  constructor(message = 'Authentication failed') {
    super(message, 401, 'AUTHENTICATION_ERROR');
  }
}

class AuthorizationError extends AppError {
  constructor(message = 'Not authorized') {
    super(message, 403, 'AUTHORIZATION_ERROR');
  }
}

class DatabaseError extends AppError {
  constructor(message, originalError) {
    super(message, 500, 'DATABASE_ERROR');
    this.originalError = originalError;
  }
}

// Usage examples
try {
  // Validation error
  const user = validateUser(req.body);
  if (!user.isValid) {
    throw new ValidationError('Invalid user data', user.errors);
  }
  
  // Resource not found
  const product = await Product.findById(id);
  if (!product) {
    throw new NotFoundError('Product');
  }
  
  // Authorization error
  if (!hasPermission(user, 'edit:products')) {
    throw new AuthorizationError('You do not have permission to edit products');
  }
} catch (err) {
  next(err); // Pass to error handling middleware
}

Creating a hierarchy of error classes has several benefits:

Synchronous Error Handling

Express automatically catches and forwards synchronous errors thrown in route handlers and middleware to your error handling middleware.

Handling Synchronous Errors

// Express automatically catches synchronous errors
app.get('/api/items/:id', (req, res) => {
  // This error will be caught automatically by Express
  const id = req.params.id;
  if (!id.match(/^[0-9a-fA-F]{24}$/)) {
    throw new ValidationError('Invalid ID format');
  }
  
  // Proceed with valid ID...
  res.json({ message: 'Valid ID format' });
});

// Example of validating query parameters
app.get('/api/search', (req, res) => {
  const { q, limit } = req.query;
  
  // Validate required parameters
  if (!q) {
    throw new ValidationError('Search query is required');
  }
  
  // Validate numeric parameters
  const parsedLimit = parseInt(limit, 10);
  if (limit && (isNaN(parsedLimit) || parsedLimit < 1)) {
    throw new ValidationError('Limit must be a positive number');
  }
  
  // Proceed with valid parameters...
  res.json({ message: 'Valid search parameters' });
});

For synchronous code, Express's built-in error handling works well without additional configuration. Simply throw errors, and they'll be caught and passed to your error handlers.

Asynchronous Error Handling

Handling errors in asynchronous code requires special attention, as they won't be automatically caught by Express.

Three Methods for Async Error Handling

Method 1: Using try/catch with async/await

// Using try/catch with async/await
app.get('/api/users/:id', async (req, res, next) => {
  try {
    const user = await User.findById(req.params.id);
    
    if (!user) {
      throw new NotFoundError('User');
    }
    
    res.json(user);
  } catch (err) {
    // Pass caught error to the error handling middleware
    next(err);
  }
});

Method 2: Using a wrapper function

// Create a utility wrapper for async route handlers
const asyncHandler = fn => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

// Use the wrapper for async routes - no try/catch needed!
app.get('/api/products/:id', asyncHandler(async (req, res) => {
  const product = await Product.findById(req.params.id);
  
  if (!product) {
    throw new NotFoundError('Product');
  }
  
  res.json(product);
}));

Method 3: Using express-async-errors package

// Install: npm install express-async-errors

// At the top of your app, import the package
const express = require('express');
require('express-async-errors'); // This patches Express to handle async errors

const app = express();

// Now async errors will be caught automatically without try/catch or wrappers
app.get('/api/orders/:id', async (req, res) => {
  const order = await Order.findById(req.params.id);
  
  if (!order) {
    throw new NotFoundError('Order');
  }
  
  res.json(order);
});

Handling Promise Rejections in Node.js

Always make sure to handle unhandled promise rejections at the application level:

// Add these handlers at the top level of your application
process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Promise Rejection:', reason);
  // Consider crashing in development to catch these early
  // In production, you might want to log and continue
});

process.on('uncaughtException', (error) => {
  console.error('Uncaught Exception:', error);
  // Gracefully shutdown the server
  server.close(() => {
    process.exit(1);
  });
  
  // If graceful shutdown fails, force exit after 1 second
  setTimeout(() => {
    process.exit(1);
  }, 1000);
});

Creating Comprehensive Error Handlers

Now that we understand different error types and how to catch them, let's create a comprehensive error handling system for Express.

Complete Error Handling System

// error-handlers.js

/**
 * Handle 404 Not Found errors
 */
function notFoundHandler(req, res, next) {
  const error = new NotFoundError(`Route not found: ${req.method} ${req.originalUrl}`);
  next(error);
}

/**
 * Handle validation errors from Mongoose/MongoDB
 */
function mongooseValidationHandler(err, req, res, next) {
  if (err.name === 'ValidationError') {
    // Extract validation errors from Mongoose
    const validationErrors = Object.values(err.errors).map(error => ({
      field: error.path,
      message: error.message
    }));
    
    // Create our custom validation error
    const validationError = new ValidationError(
      'Validation failed',
      validationErrors
    );
    
    return next(validationError);
  }
  
  // Not a Mongoose validation error, pass to next handler
  next(err);
}

/**
 * Handle MongoDB duplicate key errors
 */
function duplicateKeyHandler(err, req, res, next) {
  if (err.name === 'MongoError' && err.code === 11000) {
    // Extract the duplicate field from the error message
    const field = Object.keys(err.keyValue)[0];
    const value = err.keyValue[field];
    
    const duplicateError = new ValidationError(
      `Duplicate value: '${value}' for field '${field}'`,
      [{ field, message: 'Value already exists' }]
    );
    
    return next(duplicateError);
  }
  
  next(err);
}

/**
 * Handle JWT authentication errors
 */
function jwtErrorHandler(err, req, res, next) {
  if (err.name === 'JsonWebTokenError') {
    return next(new AuthenticationError('Invalid token'));
  }
  
  if (err.name === 'TokenExpiredError') {
    return next(new AuthenticationError('Token expired'));
  }
  
  next(err);
}

/**
 * Development error handler - includes stack trace and details
 */
function developmentErrorHandler(err, req, res, next) {
  // Log error for debugging
  console.error('ERROR STACK:', err.stack);
  
  // Get status code from error or default to 500
  const statusCode = err.statusCode || 500;
  
  // Prepare the error response with details
  const errorResponse = {
    error: {
      message: err.message || 'Internal Server Error',
      type: err.type || 'SERVER_ERROR',
      stack: err.stack,
      ...(err.invalidFields && { invalidFields: err.invalidFields }),
      ...(err.resource && { resource: err.resource })
    }
  };
  
  res.status(statusCode).json(errorResponse);
}

/**
 * Production error handler - hides implementation details
 */
function productionErrorHandler(err, req, res, next) {
  // Log error for monitoring but don't expose details
  console.error('ERROR:', {
    message: err.message,
    type: err.type || 'SERVER_ERROR',
    statusCode: err.statusCode || 500,
    isOperational: err.isOperational || false,
    url: req.originalUrl,
    method: req.method,
    ip: req.ip,
    timestamp: new Date().toISOString()
  });
  
  // Get status code from error or default to 500
  const statusCode = err.statusCode || 500;
  
  // Check if this is an operational error we expected
  // and can safely show to users
  const isOperationalError = err.isOperational === true;
  
  // Prepare the error response
  const errorResponse = {
    error: {
      message: isOperationalError
        ? err.message
        : 'Something went wrong. Please try again later.',
      type: isOperationalError
        ? err.type || 'SERVER_ERROR'
        : 'SERVER_ERROR'
    }
  };
  
  // Add validation errors if applicable
  if (err.invalidFields && isOperationalError) {
    errorResponse.error.invalidFields = err.invalidFields;
  }
  
  // Add resource info for not found errors
  if (err.resource && isOperationalError) {
    errorResponse.error.resource = err.resource;
  }
  
  // Send the appropriate response
  res.status(statusCode).json(errorResponse);
}

// Choose error handler based on environment
const finalErrorHandler = process.env.NODE_ENV === 'production'
  ? productionErrorHandler
  : developmentErrorHandler;

module.exports = {
  notFoundHandler,
  mongooseValidationHandler,
  duplicateKeyHandler,
  jwtErrorHandler,
  finalErrorHandler
};

Applying Error Handlers in Express App

// app.js
const express = require('express');
const errorHandlers = require('./error-handlers');
require('express-async-errors');

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'));
app.use('/api/orders', require('./routes/orders'));

// Apply error handling middleware in the correct order
// 1. Route not found handler (404)
app.use(errorHandlers.notFoundHandler);

// 2. Specific error type handlers
app.use(errorHandlers.mongooseValidationHandler);
app.use(errorHandlers.duplicateKeyHandler);
app.use(errorHandlers.jwtErrorHandler);

// 3. Final error handler (different for dev/prod)
app.use(errorHandlers.finalErrorHandler);

module.exports = app;

This comprehensive approach ensures that:

Error Logging

Proper error logging is essential for debugging issues in production and monitoring application health.

Advanced Error Logging with Winston

// logger.js
const winston = require('winston');
const { format, transports } = winston;

// Define log format
const logFormat = format.printf(({ level, message, timestamp, ...meta }) => {
  return `${timestamp} [${level.toUpperCase()}]: ${message} ${
    Object.keys(meta).length ? JSON.stringify(meta) : ''
  }`;
});

// Create logger instance
const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: format.combine(
    format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
    format.errors({ stack: true }),
    logFormat
  ),
  defaultMeta: { service: 'api-service' },
  transports: [
    // Write logs with level 'error' and below to error.log
    new transports.File({ filename: 'logs/error.log', level: 'error' }),
    // Write all logs to combined.log
    new transports.File({ filename: 'logs/combined.log' })
  ]
});

// If we're not in production, also log to the console
if (process.env.NODE_ENV !== 'production') {
  logger.add(new transports.Console({
    format: format.combine(
      format.colorize(),
      format.simple()
    )
  }));
}

// Create error logging middleware
const errorLogger = (err, req, res, next) => {
  // Log the error with context
  logger.error(`${err.message}`, {
    error: {
      type: err.type || err.name,
      statusCode: err.statusCode || 500,
      stack: err.stack
    },
    request: {
      url: req.originalUrl,
      method: req.method,
      params: req.params,
      query: req.query,
      body: process.env.NODE_ENV === 'development' ? req.body : '[REDACTED]',
      ip: req.ip,
      user: req.user ? req.user.id : 'anonymous'
    }
  });

  next(err);
};

module.exports = {
  logger,
  errorLogger
};

Using the Error Logger in Express

// app.js
const express = require('express');
const { errorLogger } = require('./logger');
const errorHandlers = require('./error-handlers');
require('express-async-errors');

const app = express();

// Body parser middleware
app.use(express.json());

// Routes
app.use('/api/users', require('./routes/users'));
// ... other routes

// Error handling middleware
// 1. Log all errors first
app.use(errorLogger);

// 2. Route not found handler
app.use(errorHandlers.notFoundHandler);

// 3. Specific error handlers
app.use(errorHandlers.mongooseValidationHandler);
// ... other specific handlers

// 4. Final error handler
app.use(errorHandlers.finalErrorHandler);

module.exports = app;

Log Security Considerations

  • Sensitive Data: Never log passwords, tokens, or personal information
  • Log Rotation: Implement log rotation to prevent excessive disk usage
  • Log Levels: Use appropriate log levels (error, warn, info, debug)
  • Log Sanitization: Sanitize log data to prevent log forging attacks
  • Log Storage: In production, consider sending logs to a centralized service

Error Monitoring in Production

For production applications, you'll want to implement error monitoring and alerting to detect and respond to issues quickly.

Error Monitoring Strategies

Integrating Sentry for Error Tracking

// sentry.js
const Sentry = require('@sentry/node');
const { ProfilingIntegration } = require('@sentry/profiling-node');

// Initialize Sentry
Sentry.init({
  dsn: process.env.SENTRY_DSN,
  environment: process.env.NODE_ENV,
  tracesSampleRate: 1.0,
  profilesSampleRate: 1.0,
  integrations: [
    new ProfilingIntegration(),
  ],
});

// Create Sentry middleware for Express
const sentryErrorHandler = Sentry.Handlers.errorHandler();

module.exports = {
  Sentry,
  sentryErrorHandler
};

Using Sentry in Express App

// app.js
const express = require('express');
const { Sentry, sentryErrorHandler } = require('./sentry');
const { errorLogger } = require('./logger');
const errorHandlers = require('./error-handlers');
require('express-async-errors');

const app = express();

// Initialize Sentry request handler (must be the first middleware)
app.use(Sentry.Handlers.requestHandler());

// Body parser middleware
app.use(express.json());

// API routes
app.use('/api/users', require('./routes/users'));
// ... other routes

// Error handling middleware
// 1. Log errors to local storage
app.use(errorLogger);

// 2. Send errors to Sentry 
app.use(Sentry.Handlers.errorHandler());

// 3. Specific error handlers
app.use(errorHandlers.mongooseValidationHandler);
app.use(errorHandlers.duplicateKeyHandler);
app.use(errorHandlers.jwtErrorHandler);

// 4. Final error handler
app.use(errorHandlers.finalErrorHandler);

module.exports = app;

Validation Error Handling

Input validation errors are among the most common in web applications, so it's worth implementing specialized handling for them.

Using express-validator for Validation

// validation.js
const { validationResult } = require('express-validator');
const { ValidationError } = require('./errors');

// Middleware to check for validation errors
const validateRequest = (req, res, next) => {
  const errors = validationResult(req);
  
  if (!errors.isEmpty()) {
    // Format errors for consistent output
    const formattedErrors = errors.array().map(error => ({
      field: error.param,
      message: error.msg,
      value: error.value
    }));
    
    // Create and throw a ValidationError
    throw new ValidationError('Validation failed', formattedErrors);
  }
  
  next();
};

module.exports = {
  validateRequest
};

Applying Validation in Routes

// users-routes.js
const express = require('express');
const { body } = require('express-validator');
const { validateRequest } = require('../validation');
const usersController = require('../controllers/users');

const router = express.Router();

// Create user validation rules
const createUserValidation = [
  body('name')
    .trim()
    .isLength({ min: 2, max: 50 })
    .withMessage('Name must be between 2 and 50 characters'),
    
  body('email')
    .isEmail()
    .normalizeEmail()
    .withMessage('Must be a valid email address'),
    
  body('password')
    .isLength({ min: 8 })
    .withMessage('Password must be at least 8 characters long')
    .matches(/\d/)
    .withMessage('Password must contain at least one number'),
    
  body('role')
    .optional()
    .isIn(['user', 'admin'])
    .withMessage('Role must be either user or admin'),
  
  // Apply validation
  validateRequest
];

// Create user route with validation
router.post('/', createUserValidation, usersController.createUser);

module.exports = router;

This approach provides:

API-Specific Error Handling

For APIs, it's important to provide machine-readable error responses that follow a consistent format.

Standard API Error Response Format

Following a standard like JSON:API or RFC 7807 "Problem Details" can improve interoperability with client applications.

JSON:API Compliant Error Handler

// api-error-handler.js
function jsonApiErrorHandler(err, req, res, next) {
  // Get status code from error or default to 500
  const statusCode = err.statusCode || 500;
  
  // Basic error object
  const errorResponse = {
    errors: [
      {
        status: String(statusCode),
        title: err.type || 'SERVER_ERROR',
        detail: err.message || 'An unexpected error occurred'
      }
    ]
  };
  
  // Add additional error details if available
  if (err.id) {
    errorResponse.errors[0].id = err.id;
  }
  
  if (err.code) {
    errorResponse.errors[0].code = err.code;
  }
  
  if (err.source) {
    errorResponse.errors[0].source = err.source;
  } else if (err.invalidFields) {
    // Convert validation errors to JSON:API format
    errorResponse.errors = err.invalidFields.map(field => ({
      status: String(statusCode),
      title: 'VALIDATION_ERROR',
      detail: field.message,
      source: {
        pointer: `/data/attributes/${field.field}`
      }
    }));
  }
  
  // In development, include the stack trace
  if (process.env.NODE_ENV !== 'production') {
    errorResponse.errors[0].meta = {
      stack: err.stack
    };
  }
  
  res.status(statusCode).json(errorResponse);
}

module.exports = jsonApiErrorHandler;

Sample JSON:API Error Response

{
  "errors": [
    {
      "status": "400",
      "title": "VALIDATION_ERROR",
      "detail": "Email must be a valid email address",
      "source": {
        "pointer": "/data/attributes/email"
      }
    },
    {
      "status": "400",
      "title": "VALIDATION_ERROR",
      "detail": "Password must be at least 8 characters long",
      "source": {
        "pointer": "/data/attributes/password"
      }
    }
  ]
}

Testing Error Handlers

Error handlers should be thoroughly tested to ensure they work as expected in various scenarios.

Unit Testing Error Handlers

// error-handlers.test.js
const { expect } = require('chai');
const sinon = require('sinon');
const { 
  notFoundHandler, 
  mongooseValidationHandler, 
  productionErrorHandler 
} = require('../error-handlers');
const { ValidationError, NotFoundError } = require('../errors');

describe('Error Handlers', function() {
  let req, res, next;
  
  beforeEach(function() {
    req = {
      method: 'GET',
      originalUrl: '/api/test',
      ip: '127.0.0.1'
    };
    
    res = {
      status: sinon.stub().returnsThis(),
      json: sinon.spy()
    };
    
    next = sinon.spy();
  });
  
  describe('notFoundHandler', function() {
    it('should create a NotFoundError and pass it to next', function() {
      notFoundHandler(req, res, next);
      
      expect(next.calledOnce).to.be.true;
      expect(next.firstCall.args[0]).to.be.instanceof(NotFoundError);
      expect(next.firstCall.args[0].message).to.include('/api/test');
      expect(next.firstCall.args[0].statusCode).to.equal(404);
    });
  });
  
  describe('mongooseValidationHandler', function() {
    it('should transform Mongoose validation errors', function() {
      const mongooseError = {
        name: 'ValidationError',
        errors: {
          email: {
            path: 'email',
            message: 'Email is invalid'
          },
          password: {
            path: 'password',
            message: 'Password is required'
          }
        }
      };
      
      mongooseValidationHandler(mongooseError, req, res, next);
      
      expect(next.calledOnce).to.be.true;
      const transformedError = next.firstCall.args[0];
      expect(transformedError).to.be.instanceof(ValidationError);
      expect(transformedError.statusCode).to.equal(400);
      expect(transformedError.invalidFields).to.have.lengthOf(2);
    });
    
    it('should pass non-Mongoose errors to next', function() {
      const error = new Error('Test error');
      
      mongooseValidationHandler(error, req, res, next);
      
      expect(next.calledOnce).to.be.true;
      expect(next.firstCall.args[0]).to.equal(error);
    });
  });
  
  describe('productionErrorHandler', function() {
    it('should hide sensitive information in production', function() {
      const error = new Error('Database connection failed: password incorrect');
      error.stack = 'Error stack trace';
      
      productionErrorHandler(error, req, res, next);
      
      expect(res.status.calledWith(500)).to.be.true;
      expect(res.json.calledOnce).to.be.true;
      
      const response = res.json.firstCall.args[0];
      expect(response.error.message).to.equal('Something went wrong. Please try again later.');
      expect(response.error.stack).to.be.undefined;
    });
    
    it('should show operational error details', function() {
      const error = new NotFoundError('User');
      error.isOperational = true;
      
      productionErrorHandler(error, req, res, next);
      
      expect(res.status.calledWith(404)).to.be.true;
      
      const response = res.json.firstCall.args[0];
      expect(response.error.message).to.equal('User not found');
      expect(response.error.type).to.equal('NOT_FOUND');
      expect(response.error.resource).to.equal('User');
    });
  });
});

Integration Testing Error Handlers

// api.test.js
const request = require('supertest');
const { expect } = require('chai');
const app = require('../app');

describe('API Error Handling', function() {
  describe('Route Not Found', function() {
    it('should return 404 for non-existent routes', function(done) {
      request(app)
        .get('/api/non-existent-route')
        .expect('Content-Type', /json/)
        .expect(404)
        .end(function(err, res) {
          if (err) return done(err);
          
          expect(res.body).to.have.property('error');
          expect(res.body.error.type).to.equal('NOT_FOUND');
          done();
        });
    });
  });
  
  describe('Validation Errors', function() {
    it('should return validation errors for invalid input', function(done) {
      request(app)
        .post('/api/users')
        .send({
          // Missing required fields and invalid email
          name: 'A', // Too short
          email: 'not-an-email'
        })
        .expect('Content-Type', /json/)
        .expect(400)
        .end(function(err, res) {
          if (err) return done(err);
          
          expect(res.body).to.have.property('error');
          expect(res.body.error.type).to.equal('VALIDATION_ERROR');
          expect(res.body.error.invalidFields).to.be.an('array');
          expect(res.body.error.invalidFields).to.have.length.greaterThan(0);
          done();
        });
    });
  });
  
  describe('Authentication Errors', function() {
    it('should return 401 for invalid authentication', function(done) {
      request(app)
        .get('/api/users/profile')
        .set('Authorization', 'Bearer invalid-token')
        .expect('Content-Type', /json/)
        .expect(401)
        .end(function(err, res) {
          if (err) return done(err);
          
          expect(res.body).to.have.property('error');
          expect(res.body.error.type).to.equal('AUTHENTICATION_ERROR');
          done();
        });
    });
  });
});

Practical Exercises

Exercise 1: Create a Basic Error Handling System

Implement error handling for a simple Express API with the following requirements:

  1. Create custom error classes for common error types (NotFound, Validation, Authorization)
  2. Implement middleware to catch synchronous errors
  3. Create a utility function for handling asynchronous errors
  4. Set up different error responses for development and production environments
  5. Implement basic error logging to the console

Test your implementation with routes that throw different types of errors.

Exercise 2: Advanced Error Handling System

Extend the basic error handling system with the following advanced features:

  1. Integrate with a structured logging library like Winston
  2. Implement validation error handling using express-validator
  3. Create handlers for database errors (using MongoDB/Mongoose as an example)
  4. Implement a standardized API error response format
  5. Add tests for your error handlers using Mocha/Chai or Jest
  6. Create a simple monitoring dashboard that displays error counts and types

Integrate these components into a sample application with user authentication and data validation.

Additional Resources

Documentation and Libraries

Articles and Best Practices