`n

Debugging Techniques Node.js - Complete Guide

Published: September 25, 2024 | Reading time: 23 minutes

Debugging Overview

Effective debugging is essential for maintaining reliable Node.js applications:

Debugging Benefits
# Debugging Benefits
- Faster issue resolution
- Better code understanding
- Improved code quality
- Reduced development time
- Enhanced troubleshooting
- Better error handling
- Team collaboration

Built-in Debugging Tools

Node.js Debugging Features

Built-in Debugging Tools
# Built-in Debugging Tools

# 1. Console Debugging
console.log('Debug message');
console.error('Error message');
console.warn('Warning message');
console.info('Info message');
console.debug('Debug message');
console.trace('Stack trace');

// Console with formatting
console.log('User ID: %s, Name: %s', userId, userName);
console.log('Object:', { userId, userName });
console.table([{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }]);

# 2. Debugger Statement
function processData(data) {
  debugger; // Execution will pause here
  const processed = data.map(item => item * 2);
  return processed;
}

# 3. Node.js Inspector
// Start with inspector
node --inspect app.js
node --inspect-brk app.js
node --inspect=0.0.0.0:9229 app.js

// Connect to inspector
// chrome://inspect

# 4. Process Debugging
process.on('uncaughtException', (error) => {
  console.error('Uncaught Exception:', error);
  console.error('Stack:', error.stack);
});

process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection:', reason);
  console.error('Promise:', promise);
});

# 5. Memory Debugging
const v8 = require('v8');

function logMemoryUsage() {
  const memUsage = process.memoryUsage();
  const heapStats = v8.getHeapStatistics();
  
  console.log('Memory Usage:', {
    rss: Math.round(memUsage.rss / 1024 / 1024) + ' MB',
    heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024) + ' MB',
    heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024) + ' MB',
    external: Math.round(memUsage.external / 1024 / 1024) + ' MB'
  });
  
  console.log('Heap Statistics:', {
    totalHeapSize: Math.round(heapStats.total_heap_size / 1024 / 1024) + ' MB',
    usedHeapSize: Math.round(heapStats.used_heap_size / 1024 / 1024) + ' MB',
    heapSizeLimit: Math.round(heapStats.heap_size_limit / 1024 / 1024) + ' MB'
  });
}

# 6. Performance Debugging
const { performance } = require('perf_hooks');

function measurePerformance(fn) {
  const start = performance.now();
  const result = fn();
  const end = performance.now();
  
  console.log(`Function executed in ${end - start} milliseconds`);
  return result;
}

// Usage
const result = measurePerformance(() => {
  return expensiveOperation();
});

# 7. Event Loop Debugging
const { performance } = require('perf_hooks');

function measureEventLoopLag() {
  const start = process.hrtime.bigint();
  
  setImmediate(() => {
    const lag = Number(process.hrtime.bigint() - start) / 1000000;
    console.log(`Event loop lag: ${lag.toFixed(2)}ms`);
  });
}

# 8. CPU Profiling
const v8 = require('v8');

function startProfiling() {
  v8.setFlagsFromString('--prof');
  console.log('CPU profiling started');
}

function stopProfiling() {
  v8.setFlagsFromString('--no-prof');
  console.log('CPU profiling stopped');
}

# 9. Heap Snapshot
const v8 = require('v8');

function takeHeapSnapshot() {
  const snapshot = v8.getHeapSnapshot();
  const filename = `heap-${Date.now()}.heapsnapshot`;
  
  const fileStream = require('fs').createWriteStream(filename);
  snapshot.pipe(fileStream);
  
  console.log(`Heap snapshot saved to ${filename}`);
}

# 10. Custom Debugging Utility
class Debugger {
  constructor(options = {}) {
    this.enabled = options.enabled || process.env.NODE_ENV === 'development';
    this.level = options.level || 'info';
    this.levels = {
      error: 0,
      warn: 1,
      info: 2,
      debug: 3
    };
  }
  
  log(level, message, ...args) {
    if (!this.enabled) return;
    if (this.levels[level] > this.levels[this.level]) return;
    
    const timestamp = new Date().toISOString();
    console.log(`[${timestamp}] ${level.toUpperCase()}: ${message}`, ...args);
  }
  
  error(message, ...args) {
    this.log('error', message, ...args);
  }
  
  warn(message, ...args) {
    this.log('warn', message, ...args);
  }
  
  info(message, ...args) {
    this.log('info', message, ...args);
  }
  
  debug(message, ...args) {
    this.log('debug', message, ...args);
  }
  
  time(label) {
    if (!this.enabled) return;
    console.time(label);
  }
  
  timeEnd(label) {
    if (!this.enabled) return;
    console.timeEnd(label);
  }
}

const debugger = new Debugger({ level: 'debug' });

Advanced Debugging Techniques

Professional Debugging Strategies

Advanced Debugging
# Advanced Debugging Techniques

# 1. Conditional Debugging
const debug = require('debug')('app:module');

function processData(data) {
  debug('Processing data:', data);
  
  if (data.length === 0) {
    debug('Empty data array');
    return [];
  }
  
  const result = data.map(item => {
    debug('Processing item:', item);
    return item * 2;
  });
  
  debug('Processing complete:', result);
  return result;
}

# 2. Async Debugging
async function asyncOperation() {
  debug('Starting async operation');
  
  try {
    const result = await fetchData();
    debug('Data fetched:', result);
    
    const processed = await processData(result);
    debug('Data processed:', processed);
    
    return processed;
  } catch (error) {
    debug('Error in async operation:', error);
    throw error;
  }
}

# 3. Error Context Debugging
class ErrorContext {
  constructor() {
    this.context = {};
  }
  
  setContext(key, value) {
    this.context[key] = value;
  }
  
  debugError(error, additionalContext = {}) {
    const errorContext = {
      ...this.context,
      ...additionalContext,
      error: {
        name: error.name,
        message: error.message,
        stack: error.stack
      },
      timestamp: new Date().toISOString(),
      processId: process.pid,
      memory: process.memoryUsage()
    };
    
    console.error('Error with context:', errorContext);
  }
}

const errorContext = new ErrorContext();

# 4. Request Debugging
function requestDebugger(req, res, next) {
  const start = Date.now();
  
  debug('Request started:', {
    method: req.method,
    url: req.url,
    headers: req.headers,
    body: req.body
  });
  
  res.on('finish', () => {
    const duration = Date.now() - start;
    debug('Request completed:', {
      method: req.method,
      url: req.url,
      status: res.statusCode,
      duration: duration + 'ms'
    });
  });
  
  next();
}

# 5. Database Debugging
class DatabaseDebugger {
  constructor() {
    this.queries = [];
    this.maxQueries = 100;
  }
  
  logQuery(query, params, duration) {
    const queryLog = {
      query,
      params,
      duration,
      timestamp: new Date().toISOString()
    };
    
    this.queries.push(queryLog);
    
    if (this.queries.length > this.maxQueries) {
      this.queries.shift();
    }
    
    debug('Database query:', queryLog);
  }
  
  getSlowQueries(threshold = 1000) {
    return this.queries.filter(q => q.duration > threshold);
  }
  
  getQueryStats() {
    const totalQueries = this.queries.length;
    const totalDuration = this.queries.reduce((sum, q) => sum + q.duration, 0);
    const averageDuration = totalQueries > 0 ? totalDuration / totalQueries : 0;
    
    return {
      totalQueries,
      totalDuration,
      averageDuration,
      slowQueries: this.getSlowQueries().length
    };
  }
}

const dbDebugger = new DatabaseDebugger();

# 6. Memory Leak Debugging
class MemoryLeakDebugger {
  constructor() {
    this.baseline = null;
    this.threshold = 50 * 1024 * 1024; // 50MB
    this.samples = [];
    this.maxSamples = 100;
  }
  
  setBaseline() {
    this.baseline = process.memoryUsage();
    console.log('Memory baseline set:', this.baseline);
  }
  
  checkForLeak() {
    if (!this.baseline) {
      console.log('No baseline set');
      return;
    }
    
    const current = process.memoryUsage();
    const increase = current.heapUsed - this.baseline.heapUsed;
    
    this.samples.push({
      timestamp: Date.now(),
      memory: current,
      increase
    });
    
    if (this.samples.length > this.maxSamples) {
      this.samples.shift();
    }
    
    if (increase > this.threshold) {
      console.warn(`Potential memory leak detected: ${Math.round(increase / 1024 / 1024)} MB increase`);
      return true;
    }
    
    return false;
  }
  
  getMemoryTrend() {
    if (this.samples.length < 2) return null;
    
    const recent = this.samples.slice(-10);
    const trend = recent.reduce((sum, sample) => sum + sample.increase, 0) / recent.length;
    
    return {
      trend,
      samples: this.samples.length,
      averageIncrease: trend
    };
  }
}

const memoryLeakDebugger = new MemoryLeakDebugger();

# 7. Performance Debugging
class PerformanceDebugger {
  constructor() {
    this.metrics = new Map();
  }
  
  startTimer(label) {
    this.metrics.set(label, {
      start: process.hrtime.bigint(),
      end: null,
      duration: null
    });
  }
  
  endTimer(label) {
    const metric = this.metrics.get(label);
    if (!metric) {
      console.warn(`Timer ${label} not found`);
      return;
    }
    
    metric.end = process.hrtime.bigint();
    metric.duration = Number(metric.end - metric.start) / 1000000; // Convert to milliseconds
    
    console.log(`Timer ${label}: ${metric.duration.toFixed(2)}ms`);
  }
  
  measureAsync(label, fn) {
    this.startTimer(label);
    return fn().finally(() => this.endTimer(label));
  }
  
  measureSync(label, fn) {
    this.startTimer(label);
    try {
      const result = fn();
      this.endTimer(label);
      return result;
    } catch (error) {
      this.endTimer(label);
      throw error;
    }
  }
  
  getMetrics() {
    const metrics = {};
    for (const [label, metric] of this.metrics) {
      if (metric.duration !== null) {
        metrics[label] = metric.duration;
      }
    }
    return metrics;
  }
}

const perfDebugger = new PerformanceDebugger();

# 8. Event Debugging
class EventDebugger {
  constructor() {
    this.events = [];
    this.maxEvents = 1000;
  }
  
  logEvent(eventName, data) {
    const event = {
      name: eventName,
      data,
      timestamp: new Date().toISOString(),
      processId: process.pid
    };
    
    this.events.push(event);
    
    if (this.events.length > this.maxEvents) {
      this.events.shift();
    }
    
    debug('Event logged:', event);
  }
  
  getEventStats() {
    const eventCounts = {};
    this.events.forEach(event => {
      eventCounts[event.name] = (eventCounts[event.name] || 0) + 1;
    });
    
    return {
      totalEvents: this.events.length,
      eventCounts,
      recentEvents: this.events.slice(-10)
    };
  }
  
  filterEvents(eventName) {
    return this.events.filter(event => event.name === eventName);
  }
}

const eventDebugger = new EventDebugger();

# 9. State Debugging
class StateDebugger {
  constructor() {
    this.states = new Map();
  }
  
  setState(key, value) {
    const previousValue = this.states.get(key);
    this.states.set(key, value);
    
    debug('State changed:', {
      key,
      previousValue,
      newValue: value,
      timestamp: new Date().toISOString()
    });
  }
  
  getState(key) {
    return this.states.get(key);
  }
  
  getAllStates() {
    const states = {};
    for (const [key, value] of this.states) {
      states[key] = value;
    }
    return states;
  }
  
  clearState(key) {
    this.states.delete(key);
    debug('State cleared:', key);
  }
}

const stateDebugger = new StateDebugger();

# 10. Comprehensive Debugging
class ComprehensiveDebugger {
  constructor(options = {}) {
    this.enabled = options.enabled || process.env.NODE_ENV === 'development';
    this.level = options.level || 'info';
    this.errorContext = new ErrorContext();
    this.memoryLeakDebugger = new MemoryLeakDebugger();
    this.perfDebugger = new PerformanceDebugger();
    this.eventDebugger = new EventDebugger();
    this.stateDebugger = new StateDebugger();
  }
  
  log(level, message, ...args) {
    if (!this.enabled) return;
    console.log(`[${new Date().toISOString()}] ${level.toUpperCase()}: ${message}`, ...args);
  }
  
  error(message, ...args) {
    this.log('error', message, ...args);
  }
  
  warn(message, ...args) {
    this.log('warn', message, ...args);
  }
  
  info(message, ...args) {
    this.log('info', message, ...args);
  }
  
  debug(message, ...args) {
    this.log('debug', message, ...args);
  }
  
  setContext(key, value) {
    this.errorContext.setContext(key, value);
  }
  
  debugError(error, additionalContext = {}) {
    this.errorContext.debugError(error, additionalContext);
  }
  
  startMemoryMonitoring() {
    this.memoryLeakDebugger.setBaseline();
    setInterval(() => {
      this.memoryLeakDebugger.checkForLeak();
    }, 30000); // Check every 30 seconds
  }
  
  startTimer(label) {
    this.perfDebugger.startTimer(label);
  }
  
  endTimer(label) {
    this.perfDebugger.endTimer(label);
  }
  
  logEvent(eventName, data) {
    this.eventDebugger.logEvent(eventName, data);
  }
  
  setState(key, value) {
    this.stateDebugger.setState(key, value);
  }
  
  getDebugReport() {
    return {
      memory: this.memoryLeakDebugger.getMemoryTrend(),
      performance: this.perfDebugger.getMetrics(),
      events: this.eventDebugger.getEventStats(),
      states: this.stateDebugger.getAllStates()
    };
  }
}

const debugger = new ComprehensiveDebugger();

Debugging Tools and Extensions

External Debugging Tools

Debugging Tools

  • Chrome DevTools
  • VS Code Debugger
  • Node.js Inspector
  • Clinic.js
  • 0x
  • ndb
  • node-inspector

Debugging Strategies

  • Systematic debugging
  • Logging strategies
  • Error tracking
  • Performance monitoring
  • Memory profiling
  • State inspection
  • Event tracing

Production Debugging

Production Debugging Strategies

Production Debugging
# Production Debugging

# 1. Production Debugging Setup
class ProductionDebugger {
  constructor() {
    this.enabled = process.env.DEBUG_ENABLED === 'true';
    this.logLevel = process.env.LOG_LEVEL || 'error';
    this.samplingRate = parseFloat(process.env.DEBUG_SAMPLING_RATE) || 0.1;
  }
  
  shouldDebug() {
    return this.enabled && Math.random() < this.samplingRate;
  }
  
  log(level, message, context = {}) {
    if (!this.shouldDebug()) return;
    
    const logEntry = {
      timestamp: new Date().toISOString(),
      level,
      message,
      context: {
        ...context,
        processId: process.pid,
        memory: process.memoryUsage(),
        uptime: process.uptime()
      }
    };
    
    console.log(JSON.stringify(logEntry));
  }
  
  error(message, context) {
    this.log('error', message, context);
  }
  
  warn(message, context) {
    this.log('warn', message, context);
  }
  
  info(message, context) {
    this.log('info', message, context);
  }
}

const prodDebugger = new ProductionDebugger();

# 2. Remote Debugging
const inspector = require('inspector');

class RemoteDebugger {
  constructor() {
    this.session = null;
    this.port = process.env.DEBUG_PORT || 9229;
  }
  
  start() {
    if (process.env.NODE_ENV === 'production' && process.env.DEBUG_ENABLED === 'true') {
      inspector.open(this.port, '0.0.0.0', true);
      console.log(`Remote debugging enabled on port ${this.port}`);
    }
  }
  
  stop() {
    if (this.session) {
      inspector.close();
      console.log('Remote debugging stopped');
    }
  }
}

const remoteDebugger = new RemoteDebugger();

# 3. Debugging Middleware
function debuggingMiddleware(req, res, next) {
  const start = Date.now();
  
  // Add debugging context
  req.debugContext = {
    requestId: req.id || Math.random().toString(36).substr(2, 9),
    method: req.method,
    url: req.url,
    userAgent: req.get('User-Agent'),
    ip: req.ip
  };
  
  // Log request start
  prodDebugger.info('Request started', req.debugContext);
  
  res.on('finish', () => {
    const duration = Date.now() - start;
    prodDebugger.info('Request completed', {
      ...req.debugContext,
      status: res.statusCode,
      duration
    });
  });
  
  next();
}

# 4. Error Debugging
function errorDebuggingMiddleware(error, req, res, next) {
  prodDebugger.error('Request error', {
    ...req.debugContext,
    error: {
      name: error.name,
      message: error.message,
      stack: error.stack
    }
  });
  
  next(error);
}

# 5. Performance Debugging
class PerformanceDebugger {
  constructor() {
    this.metrics = new Map();
    this.threshold = 1000; // 1 second
  }
  
  measure(label, fn) {
    const start = process.hrtime.bigint();
    
    return fn().finally(() => {
      const end = process.hrtime.bigint();
      const duration = Number(end - start) / 1000000;
      
      if (duration > this.threshold) {
        prodDebugger.warn('Slow operation detected', {
          operation: label,
          duration: duration + 'ms'
        });
      }
    });
  }
}

const perfDebugger = new PerformanceDebugger();

# 6. Memory Debugging
class MemoryDebugger {
  constructor() {
    this.threshold = 100 * 1024 * 1024; // 100MB
    this.lastCheck = Date.now();
  }
  
  check() {
    const now = Date.now();
    if (now - this.lastCheck < 60000) return; // Check every minute
    
    const memory = process.memoryUsage();
    
    if (memory.heapUsed > this.threshold) {
      prodDebugger.warn('High memory usage detected', {
        memory: {
          rss: Math.round(memory.rss / 1024 / 1024) + ' MB',
          heapUsed: Math.round(memory.heapUsed / 1024 / 1024) + ' MB',
          heapTotal: Math.round(memory.heapTotal / 1024 / 1024) + ' MB'
        }
      });
    }
    
    this.lastCheck = now;
  }
}

const memoryDebugger = new MemoryDebugger();

# 7. Debugging Health Check
app.get('/debug/health', (req, res) => {
  const health = {
    status: 'healthy',
    timestamp: new Date().toISOString(),
    uptime: process.uptime(),
    memory: process.memoryUsage(),
    debugEnabled: prodDebugger.enabled,
    logLevel: prodDebugger.logLevel,
    samplingRate: prodDebugger.samplingRate
  };
  
  res.json(health);
});

# 8. Debugging Metrics
app.get('/debug/metrics', (req, res) => {
  const metrics = {
    timestamp: new Date().toISOString(),
    process: {
      pid: process.pid,
      uptime: process.uptime(),
      memory: process.memoryUsage()
    },
    debug: {
      enabled: prodDebugger.enabled,
      logLevel: prodDebugger.logLevel,
      samplingRate: prodDebugger.samplingRate
    }
  };
  
  res.json(metrics);
});

# 9. Debugging Logs
app.get('/debug/logs', (req, res) => {
  // In production, this should be secured
  if (process.env.NODE_ENV === 'production' && !req.headers.authorization) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  
  const logs = {
    timestamp: new Date().toISOString(),
    logs: [] // This would be populated from a log store
  };
  
  res.json(logs);
});

# 10. Debugging Configuration
const debuggingConfig = {
  enabled: process.env.DEBUG_ENABLED === 'true',
  logLevel: process.env.LOG_LEVEL || 'error',
  samplingRate: parseFloat(process.env.DEBUG_SAMPLING_RATE) || 0.1,
  port: process.env.DEBUG_PORT || 9229,
  memoryThreshold: parseInt(process.env.MEMORY_THRESHOLD) || 100 * 1024 * 1024,
  performanceThreshold: parseInt(process.env.PERFORMANCE_THRESHOLD) || 1000
};

module.exports = {
  debuggingConfig,
  prodDebugger,
  remoteDebugger,
  perfDebugger,
  memoryDebugger
};

Summary

Node.js debugging techniques involve several key components:

  • Built-in Tools: Console, debugger, inspector, and profiling
  • Advanced Techniques: Conditional debugging, async debugging, and error context
  • External Tools: Chrome DevTools, VS Code, and specialized debugging tools
  • Production Debugging: Remote debugging, monitoring, and health checks

Need More Help?

Struggling with debugging techniques or need help implementing comprehensive debugging strategies? Our Node.js experts can help you master debugging and troubleshooting.

Get Debugging Help