Skip to content

Express Error Handling

Comprehensive error handling patterns for Express applications using Response Handler.

Global Error Handler

Setting Up Error Handling

javascript
const express = require('express');
const { quickSetup } = require('response-handler');

const app = express();

// Response Handler middleware
app.use(
  quickSetup({
    enableLogging: true,
    logLevel: 'info',
    environment: process.env.NODE_ENV || 'development',
  }),
);

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

// 404 handler - must come after all routes
app.use('*', (req, res) => {
  res.notFound(
    {
      path: req.originalUrl,
      method: req.method,
      availableEndpoints: ['GET /api/users', 'POST /api/users', 'GET /api/posts'],
    },
    'Route not found',
  );
});

// Global error handler - must be last middleware
app.use((error, req, res, next) => {
  // Log error details
  console.error('Unhandled error:', {
    message: error.message,
    stack: error.stack,
    url: req.url,
    method: req.method,
    body: req.body,
    user: req.user?.id,
  });

  // Handle specific error types
  if (error.type === 'entity.parse.failed') {
    return res.badRequest(
      {
        error: 'Invalid JSON',
        details: error.message,
      },
      'Request body contains invalid JSON',
    );
  }

  if (error.type === 'entity.too.large') {
    return res.badRequest(
      {
        maxSize: '10mb',
        receivedSize: error.length,
      },
      'Request body too large',
    );
  }

  // Production vs Development error responses
  if (process.env.NODE_ENV === 'production') {
    // Don't leak error details in production
    res.error({}, 'Internal server error');
  } else {
    // Show full error details in development
    res.error(
      {
        message: error.message,
        stack: error.stack,
        name: error.name,
      },
      'Internal server error',
    );
  }
});

module.exports = app;

Async Error Handling

Async Route Wrapper

javascript
// utils/asyncHandler.js
const asyncHandler = (fn) => {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
};

module.exports = asyncHandler;

Using Async Handler

javascript
const asyncHandler = require('../utils/asyncHandler');
const User = require('../models/User');

// Async route with proper error handling
app.get(
  '/api/users/:id',
  asyncHandler(async (req, res) => {
    const userId = req.params.id;

    // Validation
    if (!userId || isNaN(userId)) {
      return res.badRequest({ userId, expectedType: 'number' }, 'Invalid user ID format');
    }

    // Database operation that might throw
    const user = await User.findById(userId);

    if (!user) {
      return res.notFound({ userId }, 'User not found');
    }

    res.ok(user, 'User retrieved successfully');
  }),
);

// Multiple async operations
app.post(
  '/api/users',
  asyncHandler(async (req, res) => {
    const { email, name, password } = req.body;

    // Parallel validation checks
    const [emailExists, isValidEmail] = await Promise.all([
      User.findByEmail(email),
      validateEmailFormat(email),
    ]);

    if (emailExists) {
      return res.conflict({ email }, 'User with this email already exists');
    }

    if (!isValidEmail) {
      return res.badRequest({ email, reason: 'Invalid format' }, 'Invalid email address');
    }

    const user = await User.create({ email, name, password });
    res.created(user, 'User created successfully');
  }),
);

Database Error Handling

MongoDB Error Handling

javascript
const mongoose = require('mongoose');

// MongoDB specific error handler
function handleMongoError(error, req, res, next) {
  if (error.name === 'ValidationError') {
    const validationErrors = Object.values(error.errors).map((err) => ({
      field: err.path,
      message: err.message,
      value: err.value,
      kind: err.kind,
    }));

    return res.badRequest({ validationErrors }, 'Validation failed');
  }

  if (error.name === 'CastError') {
    return res.badRequest(
      {
        field: error.path,
        value: error.value,
        expectedType: error.kind,
      },
      `Invalid ${error.path} format`,
    );
  }

  if (error.code === 11000) {
    const field = Object.keys(error.keyPattern)[0];
    const value = error.keyValue[field];

    return res.conflict({ field, value }, `${field} already exists`);
  }

  if (error.name === 'MongoTimeoutError') {
    return res.serviceUnavailable({ timeout: error.timeout }, 'Database connection timeout');
  }

  next(error);
}

app.use(handleMongoError);

PostgreSQL Error Handling

javascript
// PostgreSQL specific error handler
function handlePostgresError(error, req, res, next) {
  if (error.code === '23505') {
    // Unique violation
    return res.conflict(
      {
        constraint: error.constraint,
        detail: error.detail,
      },
      'Resource already exists',
    );
  }

  if (error.code === '23503') {
    // Foreign key violation
    return res.badRequest(
      {
        constraint: error.constraint,
        detail: error.detail,
      },
      'Referenced resource does not exist',
    );
  }

  if (error.code === '23502') {
    // Not null violation
    return res.badRequest(
      {
        column: error.column,
        table: error.table,
      },
      'Required field is missing',
    );
  }

  if (error.code === 'ECONNREFUSED') {
    return res.serviceUnavailable({ database: 'PostgreSQL' }, 'Database connection failed');
  }

  next(error);
}

app.use(handlePostgresError);

Authentication Error Handling

JWT Error Handling

javascript
const jwt = require('jsonwebtoken');

function authenticateToken(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];

  if (!token) {
    return res.unauthorized(
      {
        authMethods: ['Bearer Token'],
        example: 'Authorization: Bearer <token>',
      },
      'Access token required',
    );
  }

  jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
    if (err) {
      if (err.name === 'TokenExpiredError') {
        return res.unauthorized(
          {
            error: 'Token expired',
            expiredAt: err.expiredAt,
            refreshEndpoint: '/api/auth/refresh',
          },
          'Token has expired',
        );
      }

      if (err.name === 'JsonWebTokenError') {
        return res.unauthorized(
          {
            error: 'Invalid token',
            reason: err.message,
          },
          'Invalid access token',
        );
      }

      if (err.name === 'NotBeforeError') {
        return res.unauthorized(
          {
            error: 'Token not active',
            notBefore: err.date,
          },
          'Token is not active yet',
        );
      }

      return res.unauthorized({ error: err.message }, 'Token verification failed');
    }

    req.user = user;
    next();
  });
}

module.exports = authenticateToken;

Validation Error Handling

Express Validator Integration

javascript
const { validationResult, body, param, query } = require('express-validator');

// Validation middleware
const handleValidationErrors = (req, res, next) => {
  const errors = validationResult(req);

  if (!errors.isEmpty()) {
    const validationErrors = errors.array().map((error) => ({
      field: error.param,
      message: error.msg,
      value: error.value,
      location: error.location,
    }));

    return res.badRequest({ validationErrors }, 'Validation failed');
  }

  next();
};

// Route with validation
app.post(
  '/api/users',
  [
    body('email').isEmail().withMessage('Must be a valid email').normalizeEmail(),
    body('password')
      .isLength({ min: 8 })
      .withMessage('Password must be at least 8 characters')
      .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
      .withMessage('Password must contain uppercase, lowercase, and number'),
    body('name')
      .trim()
      .isLength({ min: 2, max: 50 })
      .withMessage('Name must be between 2 and 50 characters'),
  ],
  handleValidationErrors,
  asyncHandler(async (req, res) => {
    const user = await User.create(req.body);
    res.created(user, 'User created successfully');
  }),
);

Joi Validation Integration

javascript
const Joi = require('joi');

// Joi validation middleware
const validateWithJoi = (schema) => {
  return (req, res, next) => {
    const { error, value } = schema.validate(req.body, {
      abortEarly: false,
      stripUnknown: true,
    });

    if (error) {
      const validationErrors = error.details.map((detail) => ({
        field: detail.path.join('.'),
        message: detail.message,
        value: detail.context?.value,
        type: detail.type,
      }));

      return res.badRequest({ validationErrors }, 'Input validation failed');
    }

    req.body = value; // Use sanitized/validated data
    next();
  };
};

// Schema definition
const userSchema = Joi.object({
  email: Joi.string().email().required(),
  password: Joi.string()
    .min(8)
    .pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
    .required(),
  name: Joi.string().trim().min(2).max(50).required(),
  age: Joi.number().integer().min(18).max(120).optional(),
});

// Route with Joi validation
app.post(
  '/api/users',
  validateWithJoi(userSchema),
  asyncHandler(async (req, res) => {
    const user = await User.create(req.body);
    res.created(user, 'User created successfully');
  }),
);

Rate Limiting Error Handling

Express Rate Limit Integration

javascript
const rateLimit = require('express-rate-limit');

// Custom rate limit handler
const createRateLimiter = (options) => {
  return rateLimit({
    ...options,
    handler: (req, res) => {
      const retryAfter = Math.round(options.windowMs / 1000);

      res.tooManyRequests(
        {
          limit: options.max,
          windowMs: options.windowMs,
          retryAfter,
          resetTime: new Date(Date.now() + options.windowMs).toISOString(),
        },
        'Too many requests, please try again later',
      );
    },
    standardHeaders: true,
    legacyHeaders: false,
  });
};

// Different rate limits for different endpoints
const generalLimiter = createRateLimiter({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // 100 requests per window
});

const strictLimiter = createRateLimiter({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 5, // 5 requests per window
});

app.use('/api/', generalLimiter);
app.use('/api/auth/', strictLimiter);

File Upload Error Handling

Multer Error Handling

javascript
const multer = require('multer');
const path = require('path');

// Multer configuration
const upload = multer({
  dest: 'uploads/',
  limits: {
    fileSize: 5 * 1024 * 1024, // 5MB
    files: 3,
  },
  fileFilter: (req, file, cb) => {
    const allowedTypes = /jpeg|jpg|png|gif|pdf/;
    const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
    const mimetype = allowedTypes.test(file.mimetype);

    if (mimetype && extname) {
      return cb(null, true);
    } else {
      cb(new Error('Invalid file type'));
    }
  },
});

// Multer error handler
function handleMulterError(error, req, res, next) {
  if (error instanceof multer.MulterError) {
    if (error.code === 'LIMIT_FILE_SIZE') {
      return res.badRequest(
        {
          maxSize: '5MB',
          receivedSize: error.field,
        },
        'File too large',
      );
    }

    if (error.code === 'LIMIT_FILE_COUNT') {
      return res.badRequest(
        {
          maxFiles: 3,
          receivedFiles: error.field,
        },
        'Too many files',
      );
    }

    if (error.code === 'LIMIT_UNEXPECTED_FILE') {
      return res.badRequest(
        {
          unexpectedField: error.field,
          allowedFields: ['avatar', 'documents'],
        },
        'Unexpected file field',
      );
    }
  }

  if (error.message === 'Invalid file type') {
    return res.badRequest(
      {
        allowedTypes: ['jpeg', 'jpg', 'png', 'gif', 'pdf'],
      },
      'Invalid file type',
    );
  }

  next(error);
}

// File upload route
app.post(
  '/api/upload',
  upload.array('files', 3),
  handleMulterError,
  asyncHandler(async (req, res) => {
    if (!req.files || req.files.length === 0) {
      return res.badRequest({ requiredField: 'files' }, 'No files uploaded');
    }

    const fileInfo = req.files.map((file) => ({
      filename: file.filename,
      originalname: file.originalname,
      size: file.size,
      path: file.path,
    }));

    res.created(fileInfo, 'Files uploaded successfully');
  }),
);

Environment-Specific Error Handling

Development vs Production

javascript
const createErrorHandler = () => {
  return (error, req, res, next) => {
    const isProduction = process.env.NODE_ENV === 'production';

    // Log error (always)
    console.error('Error occurred:', {
      message: error.message,
      stack: error.stack,
      url: req.url,
      method: req.method,
      ip: req.ip,
      userAgent: req.get('User-Agent'),
      timestamp: new Date().toISOString(),
    });

    // Send to monitoring service in production
    if (isProduction) {
      sendToMonitoringService(error, {
        url: req.url,
        method: req.method,
        user: req.user?.id,
      });
    }

    // Response based on environment
    if (isProduction) {
      // Generic error response
      res.error(
        {
          errorId: generateErrorId(),
          timestamp: new Date().toISOString(),
        },
        'Internal server error',
      );
    } else {
      // Detailed error response for development
      res.error(
        {
          message: error.message,
          stack: error.stack,
          name: error.name,
          code: error.code,
        },
        'Development error details',
      );
    }
  };
};

function generateErrorId() {
  return Date.now().toString(36) + Math.random().toString(36).substr(2);
}

function sendToMonitoringService(error, context) {
  // Integration with Sentry, LogRocket, etc.
  // sentry.captureException(error, { extra: context });
}

app.use(createErrorHandler());

This comprehensive error handling system ensures that all types of errors are properly caught, logged, and responded to with appropriate HTTP status codes and Response Handler methods.

Released under the ISC License.