Skip to content

Error Handling

Overview

Robust error handling is crucial for Node.js applications. This chapter covers error types, handling strategies, logging, monitoring, and recovery patterns to build resilient applications.

Error Types and Patterns

Basic Error Handling

javascript
// basic-error-handling.js

// Synchronous error handling
function divideNumbers(a, b) {
  if (typeof a !== 'number' || typeof b !== 'number') {
    throw new TypeError('Both arguments must be numbers');
  }
  
  if (b === 0) {
    throw new Error('Division by zero is not allowed');
  }
  
  return a / b;
}

// Try-catch for synchronous operations
try {
  const result = divideNumbers(10, 2);
  console.log('Result:', result);
} catch (error) {
  console.error('Error:', error.message);
}

// Asynchronous error handling with callbacks
function readFileCallback(filename, callback) {
  const fs = require('fs');
  
  fs.readFile(filename, 'utf8', (error, data) => {
    if (error) {
      return callback(error, null);
    }
    callback(null, data);
  });
}

// Promise-based error handling
function readFilePromise(filename) {
  const fs = require('fs').promises;
  
  return fs.readFile(filename, 'utf8')
    .catch(error => {
      throw new Error(`Failed to read file ${filename}: ${error.message}`);
    });
}

// Async/await error handling
async function processFile(filename) {
  try {
    const data = await readFilePromise(filename);
    return data.toUpperCase();
  } catch (error) {
    console.error('File processing error:', error.message);
    throw error;
  }
}

Custom Error Classes

javascript
// custom-errors.js

// Base application error
class AppError extends Error {
  constructor(message, statusCode = 500, isOperational = true) {
    super(message);
    this.name = this.constructor.name;
    this.statusCode = statusCode;
    this.isOperational = isOperational;
    
    Error.captureStackTrace(this, this.constructor);
  }
}

// Specific error types
class ValidationError extends AppError {
  constructor(message, field = null) {
    super(message, 400);
    this.field = field;
  }
}

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

class UnauthorizedError extends AppError {
  constructor(message = 'Unauthorized access') {
    super(message, 401);
  }
}

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

// Usage examples
function validateUser(userData) {
  if (!userData.email) {
    throw new ValidationError('Email is required', 'email');
  }
  
  if (!userData.email.includes('@')) {
    throw new ValidationError('Invalid email format', 'email');
  }
  
  return true;
}

function findUser(id) {
  // Simulate database lookup
  if (id !== '1') {
    throw new NotFoundError('User');
  }
  
  return { id: '1', name: 'John Doe' };
}

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

Express Error Handling

Centralized Error Handler

javascript
// express-error-handler.js
const express = require('express');
const { AppError } = require('./custom-errors');

const app = express();
app.use(express.json());

// Async wrapper to catch errors
const asyncHandler = (fn) => {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
};

// Sample routes with errors
app.get('/error/sync', (req, res, next) => {
  throw new Error('Synchronous error');
});

app.get('/error/async', asyncHandler(async (req, res, next) => {
  throw new Error('Asynchronous error');
}));

app.get('/error/validation', (req, res, next) => {
  const error = new AppError('Validation failed', 400);
  next(error);
});

// 404 handler (must be before error handler)
app.use('*', (req, res, next) => {
  const error = new AppError(`Route ${req.originalUrl} not found`, 404);
  next(error);
});

// Global error handler (must be last)
app.use((error, req, res, next) => {
  // Set default error properties
  error.statusCode = error.statusCode || 500;
  error.status = error.status || 'error';

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

  // Send error response
  if (process.env.NODE_ENV === 'development') {
    res.status(error.statusCode).json({
      status: error.status,
      error: error,
      message: error.message,
      stack: error.stack
    });
  } else {
    // Production error response
    if (error.isOperational) {
      res.status(error.statusCode).json({
        status: error.status,
        message: error.message
      });
    } else {
      // Programming error - don't leak details
      res.status(500).json({
        status: 'error',
        message: 'Something went wrong'
      });
    }
  }
});

app.listen(3000);

Logging and Monitoring

Advanced Logging System

javascript
// logging-system.js
const winston = require('winston');
const path = require('path');

class Logger {
  constructor(options = {}) {
    this.logger = this.createLogger(options);
  }

  createLogger(options) {
    const logFormat = winston.format.combine(
      winston.format.timestamp(),
      winston.format.errors({ stack: true }),
      winston.format.printf(({ timestamp, level, message, stack, ...meta }) => {
        let log = `${timestamp} [${level.toUpperCase()}]: ${message}`;
        
        if (Object.keys(meta).length > 0) {
          log += ` ${JSON.stringify(meta)}`;
        }
        
        if (stack) {
          log += `\n${stack}`;
        }
        
        return log;
      })
    );

    const transports = [
      new winston.transports.File({
        filename: path.join('logs', 'error.log'),
        level: 'error',
        maxsize: 5242880, // 5MB
        maxFiles: 5
      }),
      new winston.transports.File({
        filename: path.join('logs', 'combined.log'),
        maxsize: 5242880,
        maxFiles: 5
      })
    ];

    if (process.env.NODE_ENV !== 'production') {
      transports.push(
        new winston.transports.Console({
          format: winston.format.combine(
            winston.format.colorize(),
            winston.format.simple()
          )
        })
      );
    }

    return winston.createLogger({
      level: options.level || 'info',
      format: logFormat,
      transports
    });
  }

  info(message, meta = {}) {
    this.logger.info(message, meta);
  }

  error(message, error = null, meta = {}) {
    const logData = { ...meta };
    
    if (error) {
      logData.error = {
        message: error.message,
        stack: error.stack,
        name: error.name
      };
    }
    
    this.logger.error(message, logData);
  }

  warn(message, meta = {}) {
    this.logger.warn(message, meta);
  }

  debug(message, meta = {}) {
    this.logger.debug(message, meta);
  }
}

module.exports = Logger;

Recovery and Resilience Patterns

Circuit Breaker Implementation

javascript
// circuit-breaker.js

class CircuitBreaker {
  constructor(fn, options = {}) {
    this.fn = fn;
    this.failureThreshold = options.failureThreshold || 5;
    this.resetTimeout = options.resetTimeout || 60000;
    this.monitoringPeriod = options.monitoringPeriod || 10000;
    
    this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
    this.failureCount = 0;
    this.lastFailureTime = null;
    this.successCount = 0;
    
    this.stats = {
      totalRequests: 0,
      successfulRequests: 0,
      failedRequests: 0,
      rejectedRequests: 0
    };
  }

  async call(...args) {
    this.stats.totalRequests++;
    
    if (this.state === 'OPEN') {
      if (this.shouldAttemptReset()) {
        this.state = 'HALF_OPEN';
        this.successCount = 0;
      } else {
        this.stats.rejectedRequests++;
        throw new Error('Circuit breaker is OPEN');
      }
    }

    try {
      const result = await this.fn(...args);
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  onSuccess() {
    this.stats.successfulRequests++;
    this.failureCount = 0;
    
    if (this.state === 'HALF_OPEN') {
      this.successCount++;
      if (this.successCount >= 3) {
        this.state = 'CLOSED';
      }
    }
  }

  onFailure() {
    this.stats.failedRequests++;
    this.failureCount++;
    this.lastFailureTime = Date.now();
    
    if (this.failureCount >= this.failureThreshold) {
      this.state = 'OPEN';
    }
  }

  shouldAttemptReset() {
    return Date.now() - this.lastFailureTime >= this.resetTimeout;
  }

  getStats() {
    return {
      ...this.stats,
      state: this.state,
      failureCount: this.failureCount,
      successRate: this.stats.totalRequests > 0 
        ? (this.stats.successfulRequests / this.stats.totalRequests * 100).toFixed(2) + '%'
        : '0%'
    };
  }

  reset() {
    this.state = 'CLOSED';
    this.failureCount = 0;
    this.successCount = 0;
    this.lastFailureTime = null;
  }
}

module.exports = CircuitBreaker;

Next Steps

In the next chapter, we'll explore file handling operations including reading, writing, streaming, and file system management.

Key Takeaways

  • Proper error handling prevents application crashes
  • Custom error classes provide better error categorization
  • Centralized error handlers improve maintainability
  • Logging helps with debugging and monitoring
  • Circuit breakers prevent cascading failures
  • Recovery patterns improve application resilience

Content is for learning and research only.