Skip to content

Custom Error Types

Advanced error handling patterns with custom error classes and Response Handler.

Custom Error Classes

Base Application Error

javascript
// errors/AppError.js
class AppError extends Error {
  constructor(message, statusCode = 500, code = 'INTERNAL_ERROR', details = {}) {
    super(message);

    this.name = this.constructor.name;
    this.statusCode = statusCode;
    this.code = code;
    this.details = details;
    this.timestamp = new Date().toISOString();
    this.isOperational = true;

    Error.captureStackTrace(this, this.constructor);
  }

  toJSON() {
    return {
      name: this.name,
      message: this.message,
      code: this.code,
      statusCode: this.statusCode,
      details: this.details,
      timestamp: this.timestamp,
      ...(process.env.NODE_ENV !== 'production' && {
        stack: this.stack,
      }),
    };
  }
}

module.exports = AppError;

Validation Errors

javascript
// errors/ValidationError.js
const AppError = require('./AppError');

class ValidationError extends AppError {
  constructor(message = 'Validation failed', validationErrors = []) {
    super(message, 400, 'VALIDATION_ERROR', {
      validationErrors,
    });
  }

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

    return new ValidationError('Input validation failed', validationErrors);
  }

  static fromExpressValidator(errors) {
    const validationErrors = errors.map((error) => ({
      field: error.param,
      message: error.msg,
      value: error.value,
      type: 'field_validation',
    }));

    return new ValidationError('Input validation failed', validationErrors);
  }

  addField(field, message, value = null) {
    this.details.validationErrors.push({
      field,
      message,
      value,
      type: 'custom_validation',
    });
    return this;
  }
}

module.exports = ValidationError;

Business Logic Errors

javascript
// errors/BusinessError.js
const AppError = require('./AppError');

class BusinessError extends AppError {
  constructor(message, code, details = {}) {
    super(message, 422, code, details);
  }
}

class InsufficientFundsError extends BusinessError {
  constructor(required, available, accountId) {
    super('Insufficient funds for this transaction', 'INSUFFICIENT_FUNDS', {
      required,
      available,
      accountId,
    });
  }
}

class AccountLockedError extends BusinessError {
  constructor(accountId, reason = 'Security violation') {
    super('Account is locked and cannot perform this action', 'ACCOUNT_LOCKED', {
      accountId,
      reason,
    });
  }
}

class DuplicateResourceError extends BusinessError {
  constructor(resource, field, value) {
    super(`${resource} with ${field} '${value}' already exists`, 'DUPLICATE_RESOURCE', {
      resource,
      field,
      value,
    });
  }
}

class ResourceNotFoundError extends BusinessError {
  constructor(resource, identifier) {
    super(`${resource} not found`, 'RESOURCE_NOT_FOUND', { resource, identifier });
    this.statusCode = 404;
  }
}

class QuotaExceededError extends BusinessError {
  constructor(quotaType, limit, current) {
    super(`${quotaType} quota exceeded`, 'QUOTA_EXCEEDED', { quotaType, limit, current });
    this.statusCode = 429;
  }
}

module.exports = {
  BusinessError,
  InsufficientFundsError,
  AccountLockedError,
  DuplicateResourceError,
  ResourceNotFoundError,
  QuotaExceededError,
};

Authentication & Authorization Errors

javascript
// errors/AuthError.js
const AppError = require('./AppError');

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

class TokenExpiredError extends AuthenticationError {
  constructor(tokenType = 'access_token') {
    super(`${tokenType} has expired`);
    this.code = 'TOKEN_EXPIRED';
    this.details = { tokenType };
  }
}

class InvalidTokenError extends AuthenticationError {
  constructor(tokenType = 'access_token') {
    super(`Invalid ${tokenType}`);
    this.code = 'INVALID_TOKEN';
    this.details = { tokenType };
  }
}

class AuthorizationError extends AppError {
  constructor(requiredRole, userRole, resource = null) {
    super('Insufficient permissions to access this resource', 403, 'AUTHORIZATION_FAILED', {
      requiredRole,
      userRole,
      resource,
    });
  }
}

class AccountNotVerifiedError extends AuthenticationError {
  constructor(email) {
    super('Account email not verified');
    this.code = 'ACCOUNT_NOT_VERIFIED';
    this.details = { email };
  }
}

module.exports = {
  AuthenticationError,
  TokenExpiredError,
  InvalidTokenError,
  AuthorizationError,
  AccountNotVerifiedError,
};

External Service Errors

javascript
// errors/ExternalError.js
const AppError = require('./AppError');

class ExternalServiceError extends AppError {
  constructor(service, message, originalError = null) {
    super(`External service error: ${message}`, 503, 'EXTERNAL_SERVICE_ERROR', {
      service,
      originalError: originalError?.message,
    });

    this.service = service;
    this.originalError = originalError;
  }
}

class PaymentServiceError extends ExternalServiceError {
  constructor(provider, transactionId, message, originalError = null) {
    super(provider, message, originalError);
    this.code = 'PAYMENT_SERVICE_ERROR';
    this.details.transactionId = transactionId;
    this.details.provider = provider;
  }
}

class EmailServiceError extends ExternalServiceError {
  constructor(provider, recipient, message, originalError = null) {
    super(provider, message, originalError);
    this.code = 'EMAIL_SERVICE_ERROR';
    this.details.recipient = recipient;
    this.details.provider = provider;
  }
}

class DatabaseConnectionError extends ExternalServiceError {
  constructor(database, operation, originalError = null) {
    super(database, `Database operation failed: ${operation}`, originalError);
    this.code = 'DATABASE_CONNECTION_ERROR';
    this.details.operation = operation;
    this.statusCode = 500;
  }
}

module.exports = {
  ExternalServiceError,
  PaymentServiceError,
  EmailServiceError,
  DatabaseConnectionError,
};

Error Handler Middleware

Enhanced Error Handler

javascript
// middleware/errorHandler.js
const { quickSetup } = require('response-handler');
const AppError = require('../errors/AppError');
const ValidationError = require('../errors/ValidationError');

function createErrorHandler(config = {}) {
  return quickSetup({
    ...config,
    customErrorHandler: (error, req, res, next) => {
      // Handle operational errors
      if (error.isOperational) {
        return handleOperationalError(error, req, res);
      }

      // Handle known error types
      if (error.name === 'ValidationError') {
        return handleValidationError(error, req, res);
      }

      if (error.name === 'CastError') {
        return handleCastError(error, req, res);
      }

      if (error.code === 11000) {
        return handleDuplicateKeyError(error, req, res);
      }

      if (error.name === 'JsonWebTokenError') {
        return handleJWTError(error, req, res);
      }

      if (error.name === 'TokenExpiredError') {
        return handleTokenExpiredError(error, req, res);
      }

      // Handle unknown errors
      return handleProgrammingError(error, req, res);
    },
  });
}

function handleOperationalError(error, req, res) {
  const statusCode = error.statusCode || 500;

  // Map status codes to response methods
  switch (statusCode) {
    case 400:
      return res.badRequest(error.toJSON(), error.message);
    case 401:
      return res.unauthorized(error.toJSON(), error.message);
    case 403:
      return res.forbidden(error.toJSON(), error.message);
    case 404:
      return res.notFound(error.toJSON(), error.message);
    case 409:
      return res.conflict(error.toJSON(), error.message);
    case 422:
      return res.unprocessableEntity(error.toJSON(), error.message);
    case 429:
      return res.tooManyRequests(error.toJSON(), error.message);
    default:
      return res.error(error.toJSON(), error.message);
  }
}

function handleValidationError(error, req, res) {
  const validationError = ValidationError.fromJoi(error);
  return res.badRequest(validationError.toJSON(), validationError.message);
}

function handleCastError(error, req, res) {
  const message = `Invalid ${error.path}: ${error.value}`;
  const customError = new ValidationError(message, [
    {
      field: error.path,
      message: `Invalid ${error.path}`,
      value: error.value,
      type: 'cast_error',
    },
  ]);

  return res.badRequest(customError.toJSON(), customError.message);
}

function handleDuplicateKeyError(error, req, res) {
  const field = Object.keys(error.keyValue)[0];
  const value = error.keyValue[field];

  const customError = new DuplicateResourceError('Resource', field, value);
  return res.conflict(customError.toJSON(), customError.message);
}

function handleJWTError(error, req, res) {
  const customError = new InvalidTokenError();
  return res.unauthorized(customError.toJSON(), customError.message);
}

function handleTokenExpiredError(error, req, res) {
  const customError = new TokenExpiredError();
  return res.unauthorized(customError.toJSON(), customError.message);
}

function handleProgrammingError(error, req, res) {
  console.error('Programming Error:', error);

  // In production, don't leak error details
  if (process.env.NODE_ENV === 'production') {
    return res.error({}, 'Something went wrong');
  }

  // In development, show full error
  return res.error(
    {
      message: error.message,
      stack: error.stack,
      name: error.name,
    },
    'Internal server error',
  );
}

module.exports = { createErrorHandler };

Usage Examples

User Service with Custom Errors

javascript
// services/UserService.js
const bcrypt = require('bcrypt');
const User = require('../models/User');
const {
  ValidationError,
  DuplicateResourceError,
  ResourceNotFoundError,
  AuthenticationError,
} = require('../errors');

class UserService {
  static async createUser(userData) {
    const { email, password, name } = userData;

    // Validate input
    if (!email || !password || !name) {
      throw new ValidationError('Missing required fields')
        .addField('email', 'Email is required')
        .addField('password', 'Password is required')
        .addField('name', 'Name is required');
    }

    // Check if user exists
    const existingUser = await User.findByEmail(email);
    if (existingUser) {
      throw new DuplicateResourceError('User', 'email', email);
    }

    // Validate password strength
    if (password.length < 8) {
      throw new ValidationError('Password validation failed').addField(
        'password',
        'Password must be at least 8 characters',
      );
    }

    try {
      const hashedPassword = await bcrypt.hash(password, 12);
      const user = await User.create({
        email,
        name,
        password: hashedPassword,
      });

      return user;
    } catch (error) {
      throw new DatabaseConnectionError('User Database', 'create user', error);
    }
  }

  static async authenticateUser(email, password) {
    if (!email || !password) {
      throw new ValidationError('Authentication data required')
        .addField('email', 'Email is required')
        .addField('password', 'Password is required');
    }

    const user = await User.findByEmail(email);
    if (!user) {
      throw new AuthenticationError('Invalid credentials');
    }

    const isValidPassword = await bcrypt.compare(password, user.password);
    if (!isValidPassword) {
      throw new AuthenticationError('Invalid credentials');
    }

    if (!user.isVerified) {
      throw new AccountNotVerifiedError(email);
    }

    return user;
  }

  static async getUserById(id) {
    if (!id) {
      throw new ValidationError('User ID is required').addField('id', 'Valid user ID is required');
    }

    const user = await User.findById(id);
    if (!user) {
      throw new ResourceNotFoundError('User', id);
    }

    return user;
  }
}

module.exports = UserService;

API Routes with Custom Errors

javascript
// routes/users.js
const express = require('express');
const UserService = require('../services/UserService');
const { authenticateToken } = require('../middleware/auth');

const router = express.Router();

// Create user
router.post('/', async (req, res, next) => {
  try {
    const user = await UserService.createUser(req.body);
    res.created(user, 'User created successfully');
  } catch (error) {
    next(error); // Error handler middleware will handle custom errors
  }
});

// Authenticate user
router.post('/auth', async (req, res, next) => {
  try {
    const { email, password } = req.body;
    const user = await UserService.authenticateUser(email, password);

    const token = generateToken(user.id);

    res.ok(
      {
        user: user.toJSON(),
        token,
      },
      'Authentication successful',
    );
  } catch (error) {
    next(error);
  }
});

// Get user
router.get('/:id', authenticateToken, async (req, res, next) => {
  try {
    const user = await UserService.getUserById(req.params.id);
    res.ok(user.toJSON(), 'User retrieved successfully');
  } catch (error) {
    next(error);
  }
});

module.exports = router;

Socket.IO with Custom Errors

javascript
// handlers/userHandler.js
const UserService = require('../services/UserService');

function handleUserEvents(io) {
  io.on('connection', (socket) => {
    socket.on('user:create', async (data) => {
      try {
        const user = await UserService.createUser(data);
        socket.created(user.toJSON(), 'User created successfully');
      } catch (error) {
        handleSocketError(socket, error);
      }
    });

    socket.on('user:authenticate', async (data) => {
      try {
        const result = await UserService.authenticateUser(data.email, data.password);
        socket.ok(result, 'Authentication successful');
      } catch (error) {
        handleSocketError(socket, error);
      }
    });
  });
}

function handleSocketError(socket, error) {
  if (error.isOperational) {
    const statusCode = error.statusCode || 500;

    switch (statusCode) {
      case 400:
        return socket.badRequest(error.toJSON(), error.message);
      case 401:
        return socket.unauthorized(error.toJSON(), error.message);
      case 403:
        return socket.forbidden(error.toJSON(), error.message);
      case 404:
        return socket.notFound(error.toJSON(), error.message);
      case 409:
        return socket.conflict(error.toJSON(), error.message);
      case 429:
        return socket.tooManyRequests(error.toJSON(), error.message);
      default:
        return socket.error(error.toJSON(), error.message);
    }
  }

  // Log programming errors
  console.error('Socket programming error:', error);

  // Don't leak error details in production
  if (process.env.NODE_ENV === 'production') {
    return socket.error({}, 'Internal server error');
  }

  return socket.error(
    {
      message: error.message,
      stack: error.stack,
    },
    'Internal server error',
  );
}

module.exports = { handleUserEvents, handleSocketError };

Error Monitoring & Logging

Error Logger

javascript
// utils/errorLogger.js
const winston = require('winston');

const errorLogger = winston.createLogger({
  level: 'error',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json(),
  ),
  transports: [
    new winston.transports.File({ filename: 'logs/error.log' }),
    new winston.transports.Console({
      format: winston.format.simple(),
    }),
  ],
});

function logError(error, context = {}) {
  const errorInfo = {
    message: error.message,
    stack: error.stack,
    code: error.code,
    statusCode: error.statusCode,
    isOperational: error.isOperational,
    timestamp: new Date().toISOString(),
    context,
  };

  errorLogger.error(errorInfo);

  // Send to external monitoring service
  if (process.env.NODE_ENV === 'production') {
    sendToMonitoringService(errorInfo);
  }
}

function sendToMonitoringService(errorInfo) {
  // Integrate with Sentry, LogRocket, etc.
  // Sentry.captureException(error);
}

module.exports = { logError };

This comprehensive custom error system provides type-safe, descriptive errors that integrate seamlessly with Response Handler for consistent API responses.

Released under the ISC License.