Skip to content

Error Handling Examples

Comprehensive examples for handling errors with Response Handler.

Basic Error Handling

Express Error Handling

typescript
import express from 'express';
import { quickSetup } from '@amit-kandar/response-handler';

const app = express();
const { middleware, errorHandler } = quickSetup({
  mode: 'development',
  logging: { logErrors: true },
});

app.use(middleware);

// Route with manual error handling
app.get('/users/:id', async (req, res) => {
  try {
    const user = await getUserById(req.params.id);

    if (!user) {
      return res.notFound({ id: req.params.id }, 'User not found');
    }

    res.ok(user, 'User retrieved successfully');
  } catch (error) {
    // Manual error handling
    if (error.name === 'ValidationError') {
      return res.badRequest(error, 'Invalid user ID format');
    }

    if (error.name === 'DatabaseError') {
      return res.internalServerError(error, 'Database connection failed');
    }

    // Generic error fallback
    res.internalServerError(error, 'An unexpected error occurred');
  }
});

// Route that throws errors (handled by error middleware)
app.post('/users', async (req, res) => {
  // These errors will be caught by the error handler
  const user = await createUser(req.body); // May throw ValidationError
  res.created(user, 'User created successfully');
});

// Error handler catches unhandled errors
app.use(errorHandler);

Socket.IO Error Handling

typescript
import { Server } from 'socket.io';
import { quickSocketSetup } from '@amit-kandar/response-handler';

const io = new Server(httpServer);
const { enhance, wrapper } = quickSocketSetup({
  mode: 'development',
  logging: { logErrors: true },
});

io.on('connection', (socket) => {
  // Manual error handling
  socket.on('get-user', (data) => {
    const response = enhance(socket, 'user-data');

    try {
      if (!data.userId) {
        return response.badRequest({ field: 'userId' }, 'User ID is required');
      }

      const user = getUserById(data.userId);
      response.ok(user, 'User retrieved');
    } catch (error) {
      response.error(error, 'Failed to get user');
    }
  });

  // Automatic error handling with wrapper
  socket.on(
    'create-post',
    wrapper(async (socket, response, data) => {
      // Any thrown error is automatically caught and handled
      const post = await createPost(data); // May throw errors
      response.created(post, 'Post created');
    }),
  );
});

Custom Error Types

Defining Custom Errors

typescript
// Custom error classes
class ValidationError extends Error {
  constructor(message, field, value) {
    super(message);
    this.name = 'ValidationError';
    this.field = field;
    this.value = value;
    this.statusCode = 400;
  }
}

class AuthenticationError extends Error {
  constructor(message, reason) {
    super(message);
    this.name = 'AuthenticationError';
    this.reason = reason;
    this.statusCode = 401;
  }
}

class NotFoundError extends Error {
  constructor(resource, id) {
    super(`${resource} not found`);
    this.name = 'NotFoundError';
    this.resource = resource;
    this.id = id;
    this.statusCode = 404;
  }
}

class BusinessLogicError extends Error {
  constructor(message, code, details) {
    super(message);
    this.name = 'BusinessLogicError';
    this.code = code;
    this.details = details;
    this.statusCode = 422;
  }
}

class RateLimitError extends Error {
  constructor(limit, windowMs) {
    super('Rate limit exceeded');
    this.name = 'RateLimitError';
    this.limit = limit;
    this.windowMs = windowMs;
    this.statusCode = 429;
  }
}

Using Custom Errors

typescript
// Service layer with custom errors
class UserService {
  static async create(userData) {
    // Validation
    if (!userData.email) {
      throw new ValidationError('Email is required', 'email', userData.email);
    }

    if (!isValidEmail(userData.email)) {
      throw new ValidationError('Invalid email format', 'email', userData.email);
    }

    // Check if user exists
    const existingUser = await User.findByEmail(userData.email);
    if (existingUser) {
      throw new BusinessLogicError('Email already registered', 'DUPLICATE_EMAIL', {
        email: userData.email,
      });
    }

    try {
      return await User.create(userData);
    } catch (dbError) {
      throw new Error('Database operation failed');
    }
  }

  static async findById(id) {
    if (!isValidId(id)) {
      throw new ValidationError('Invalid ID format', 'id', id);
    }

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

    return user;
  }

  static async authenticate(token) {
    if (!token) {
      throw new AuthenticationError('Token required', 'MISSING_TOKEN');
    }

    try {
      const decoded = jwt.verify(token, JWT_SECRET);
      return decoded;
    } catch (jwtError) {
      throw new AuthenticationError('Invalid token', 'INVALID_TOKEN');
    }
  }
}

// Controller using custom errors
app.post('/users', async (req, res) => {
  try {
    const user = await UserService.create(req.body);
    res.created(user, 'User created successfully');
  } catch (error) {
    // Custom errors are automatically handled with correct status codes
    throw error; // Will be caught by error handler
  }
});

Error Handler Customization

Custom Error Handler

typescript
import { ResponseHandler } from '@amit-kandar/response-handler';

class CustomResponseHandler extends ResponseHandler {
  errorHandler() {
    return (err, req, res, next) => {
      // Custom error logging
      this.logger.error('Custom error handler', err, {
        method: req.method,
        url: req.url,
        userAgent: req.get('User-Agent'),
        ip: req.ip,
      });

      // Custom error categorization
      if (err.name === 'ValidationError') {
        return res.badRequest(
          {
            type: 'validation',
            field: err.field,
            value: err.value,
            constraint: err.constraint,
          },
          err.message,
        );
      }

      if (err.name === 'AuthenticationError') {
        return res.unauthorized(
          {
            type: 'authentication',
            reason: err.reason,
          },
          err.message,
        );
      }

      if (err.name === 'AuthorizationError') {
        return res.forbidden(
          {
            type: 'authorization',
            required: err.required,
            actual: err.actual,
          },
          err.message,
        );
      }

      if (err.name === 'BusinessLogicError') {
        return res.unprocessableEntity(
          {
            type: 'business_logic',
            code: err.code,
            details: err.details,
          },
          err.message,
        );
      }

      if (err.name === 'RateLimitError') {
        return res.tooManyRequests(
          {
            type: 'rate_limit',
            limit: err.limit,
            windowMs: err.windowMs,
            retryAfter: Math.ceil(err.windowMs / 1000),
          },
          err.message,
        );
      }

      // Default error handling
      return res.internalServerError(err, 'An unexpected error occurred');
    };
  }
}

Validation Errors

With Joi Validation

typescript
import Joi from 'joi';

const userSchema = Joi.object({
  name: Joi.string().min(2).max(50).required(),
  email: Joi.string().email().required(),
  age: Joi.number().min(18).max(100),
  role: Joi.string().valid('user', 'admin').default('user'),
});

// Validation middleware
const validateUser = (req, res, next) => {
  const { error, value } = userSchema.validate(req.body, { abortEarly: false });

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

    return res.badRequest(
      {
        type: 'validation',
        errors: validationErrors,
      },
      'Validation failed',
    );
  }

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

app.post('/users', validateUser, async (req, res) => {
  const user = await UserService.create(req.body);
  res.created(user, 'User created successfully');
});

With Express Validator

typescript
import { body, validationResult } from 'express-validator';

// Validation rules
const userValidationRules = [
  body('email').isEmail().normalizeEmail(),
  body('password')
    .isLength({ min: 8 })
    .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/),
  body('name').trim().isLength({ min: 2, max: 50 }),
  body('age').optional().isInt({ min: 18, max: 100 }),
];

// Validation handler
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(
      {
        type: 'validation',
        errors: validationErrors,
      },
      'Input validation failed',
    );
  }

  next();
};

app.post('/users', userValidationRules, handleValidationErrors, async (req, res) => {
  const user = await UserService.create(req.body);
  res.created(user, 'User created successfully');
});

Database Errors

Handling Database Errors

typescript
// Database service with error handling
class DatabaseService {
  static async findById(model, id) {
    try {
      const result = await model.findById(id);
      if (!result) {
        throw new NotFoundError(model.name, id);
      }
      return result;
    } catch (error) {
      if (error.name === 'CastError') {
        throw new ValidationError('Invalid ID format', 'id', id);
      }
      if (error.name === 'NotFoundError') {
        throw error;
      }
      throw new Error('Database query failed');
    }
  }

  static async create(model, data) {
    try {
      return await model.create(data);
    } catch (error) {
      if (error.code === 11000) {
        // MongoDB duplicate key
        const field = Object.keys(error.keyValue)[0];
        throw new BusinessLogicError(`${field} already exists`, 'DUPLICATE_KEY', {
          field,
          value: error.keyValue[field],
        });
      }

      if (error.name === 'ValidationError') {
        const validationErrors = Object.values(error.errors).map((err) => ({
          field: err.path,
          message: err.message,
          value: err.value,
        }));

        throw new ValidationError('Validation failed', 'multiple', validationErrors);
      }

      throw new Error('Database operation failed');
    }
  }
}

// Usage in routes
app.post('/users', async (req, res) => {
  const user = await DatabaseService.create(User, req.body);
  res.created(user, 'User created successfully');
});

app.get('/users/:id', async (req, res) => {
  const user = await DatabaseService.findById(User, req.params.id);
  res.ok(user, 'User retrieved successfully');
});

Authentication & Authorization Errors

JWT Authentication

typescript
import jwt from 'jsonwebtoken';

// Authentication middleware
const authenticate = (req, res, next) => {
  const token = req.headers.authorization?.replace('Bearer ', '');

  if (!token) {
    return res.unauthorized(
      {
        type: 'authentication',
        reason: 'missing_token',
      },
      'Authentication token required',
    );
  }

  try {
    const decoded = jwt.verify(token, JWT_SECRET);
    req.user = decoded;
    next();
  } catch (jwtError) {
    let reason = 'invalid_token';
    let message = 'Invalid authentication token';

    if (jwtError.name === 'TokenExpiredError') {
      reason = 'token_expired';
      message = 'Authentication token has expired';
    } else if (jwtError.name === 'JsonWebTokenError') {
      reason = 'malformed_token';
      message = 'Malformed authentication token';
    }

    return res.unauthorized(
      {
        type: 'authentication',
        reason,
        expiredAt: jwtError.expiredAt,
      },
      message,
    );
  }
};

// Authorization middleware
const authorize = (roles) => (req, res, next) => {
  if (!req.user) {
    return res.unauthorized(
      {
        type: 'authentication',
      },
      'Authentication required',
    );
  }

  if (!roles.includes(req.user.role)) {
    return res.forbidden(
      {
        type: 'authorization',
        required: roles,
        actual: req.user.role,
      },
      'Insufficient permissions',
    );
  }

  next();
};

// Usage
app.get('/admin/users', authenticate, authorize(['admin']), async (req, res) => {
  const users = await UserService.findAll();
  res.ok(users, 'Users retrieved successfully');
});

Rate Limiting Errors

Custom Rate Limiting

typescript
import Redis from 'redis';

const redis = Redis.createClient();

// Rate limiting middleware
const rateLimit = (options) => async (req, res, next) => {
  const { windowMs, max, keyGenerator } = options;
  const key = keyGenerator ? keyGenerator(req) : req.ip;
  const windowKey = `rate_limit:${key}:${(Date.now() / windowMs) | 0}`;

  try {
    const current = await redis.incr(windowKey);

    if (current === 1) {
      await redis.expire(windowKey, Math.ceil(windowMs / 1000));
    }

    if (current > max) {
      const ttl = await redis.ttl(windowKey);

      return res.tooManyRequests(
        {
          type: 'rate_limit',
          limit: max,
          current,
          windowMs,
          retryAfter: ttl,
        },
        'Too many requests, please try again later',
      );
    }

    // Add rate limit headers
    res.setHeader('X-RateLimit-Limit', max);
    res.setHeader('X-RateLimit-Remaining', Math.max(0, max - current));
    res.setHeader('X-RateLimit-Reset', Date.now() + ttl * 1000);

    next();
  } catch (error) {
    // If Redis is down, allow the request but log the error
    console.error('Rate limiting error:', error);
    next();
  }
};

// Usage
app.use(
  '/api/',
  rateLimit({
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: 100, // limit each IP to 100 requests per windowMs
    keyGenerator: (req) => req.user?.id || req.ip, // Rate limit by user ID if authenticated
  }),
);

Error Recovery & Retry Logic

Automatic Retry with Exponential Backoff

typescript
// Retry utility function
async function withRetry(operation, maxRetries = 3, baseDelay = 1000) {
  let lastError;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await operation();
    } catch (error) {
      lastError = error;

      // Don't retry client errors (4xx)
      if (error.statusCode >= 400 && error.statusCode < 500) {
        throw error;
      }

      if (attempt === maxRetries) {
        throw lastError;
      }

      // Exponential backoff with jitter
      const delay = baseDelay * Math.pow(2, attempt) + Math.random() * 1000;
      await new Promise((resolve) => setTimeout(resolve, delay));
    }
  }
}

// Usage in service
class ExternalAPIService {
  static async fetchUserData(userId) {
    return withRetry(
      async () => {
        const response = await fetch(`${API_BASE}/users/${userId}`);

        if (!response.ok) {
          const error = new Error(`API request failed: ${response.statusText}`);
          error.statusCode = response.status;
          throw error;
        }

        return response.json();
      },
      3,
      1000,
    );
  }
}

// Route with retry logic
app.get('/users/:id/external', async (req, res) => {
  try {
    const userData = await ExternalAPIService.fetchUserData(req.params.id);
    res.ok(userData, 'External user data retrieved');
  } catch (error) {
    if (error.statusCode === 404) {
      return res.notFound({ id: req.params.id }, 'User not found in external system');
    }

    res.internalServerError(error, 'Failed to fetch external user data');
  }
});

Error Monitoring & Alerting

Error Tracking Integration

typescript
import * as Sentry from '@sentry/node';

// Initialize Sentry
Sentry.init({
  dsn: process.env.SENTRY_DSN,
  environment: process.env.NODE_ENV,
});

// Custom error handler with Sentry integration
const customErrorHandler = (config) => (err, req, res, next) => {
  // Log to Sentry for server errors
  if (err.statusCode >= 500 || !err.statusCode) {
    Sentry.withScope((scope) => {
      scope.setTag('component', 'error_handler');
      scope.setUser({
        id: req.user?.id,
        email: req.user?.email,
      });
      scope.setContext('request', {
        method: req.method,
        url: req.url,
        headers: req.headers,
        body: req.body,
      });
      Sentry.captureException(err);
    });
  }

  // Continue with normal error handling
  const builder = new ResponseBuilder(config, logger, req, res);

  if (err.statusCode) {
    return builder.respond(err.statusCode, undefined, err.message, err);
  }

  return builder.internalServerError(err, 'An unexpected error occurred');
};

// Health check endpoint with error monitoring
app.get('/health', async (req, res) => {
  try {
    // Check database connection
    await database.ping();

    // Check Redis connection
    await redis.ping();

    // Check external APIs
    await Promise.all([checkExternalAPI('payments'), checkExternalAPI('notifications')]);

    res.ok(
      {
        status: 'healthy',
        timestamp: new Date().toISOString(),
        services: ['database', 'redis', 'payments', 'notifications'],
      },
      'All systems operational',
    );
  } catch (error) {
    res.internalServerError(
      {
        status: 'unhealthy',
        error: error.message,
        timestamp: new Date().toISOString(),
      },
      'System health check failed',
    );
  }
});

This comprehensive error handling guide covers most scenarios you'll encounter when building robust applications with Response Handler.

Released under the ISC License.