Advanced JavaScriptFeatured

JavaScript Debugging: Complete Developer Guide

Master debugging techniques in JavaScript. Learn browser DevTools, debugging strategies, error tracking, and performance profiling.

By JavaScriptDoc Team
debuggingdevtoolsjavascriptperformancetroubleshooting

JavaScript Debugging: Complete Developer Guide

Debugging is an essential skill for JavaScript developers. This guide covers tools, techniques, and best practices for effectively debugging JavaScript applications.

Browser Developer Tools

Console Methods

// Basic console methods
console.log('Basic log message');
console.info('Information message');
console.warn('Warning message');
console.error('Error message');

// Formatting console output
console.log('Hello %s, you have %d points', 'John', 100);
console.log(
  '%c Styled text',
  'color: blue; font-size: 20px; font-weight: bold'
);

// Multiple values
console.log('Multiple', 'values', 'in', 'one', 'call');

// Object and array inspection
const user = { name: 'John', age: 30, hobbies: ['reading', 'gaming'] };
console.log(user);
console.dir(user); // Interactive object display
console.table(user); // Tabular display

// Grouping console output
console.group('User Details');
console.log('Name:', user.name);
console.log('Age:', user.age);
console.groupCollapsed('Hobbies');
user.hobbies.forEach((hobby) => console.log(hobby));
console.groupEnd();
console.groupEnd();

// Assertions
console.assert(user.age > 0, 'Age must be positive');
console.assert(user.name === 'Jane', 'Name is not Jane'); // Will show error

// Timing operations
console.time('Loop timer');
for (let i = 0; i < 1000000; i++) {
  // Some operation
}
console.timeEnd('Loop timer');

// Counting
function processItem(item) {
  console.count('processItem called');
  // Process the item
}

// Stack traces
console.trace('Trace point');

// Clear console
// console.clear();

Advanced Console Techniques

// Custom console styling
const styles = [
  'color: green',
  'background: yellow',
  'font-size: 30px',
  'border: 1px solid red',
  'text-shadow: 2px 2px black',
  'padding: 10px',
].join(';');

console.log('%c Success! ', styles);

// Console profiling
console.profile('MyProfile');
// Code to profile
for (let i = 0; i < 1000; i++) {
  document.createElement('div');
}
console.profileEnd('MyProfile');

// Memory usage
if (console.memory) {
  console.log('Memory usage:', console.memory);
}

// Custom console wrapper
class Logger {
  constructor(prefix = '') {
    this.prefix = prefix;
    this.enabled = true;
  }

  log(...args) {
    if (this.enabled) {
      console.log(`[${this.prefix}]`, ...args);
    }
  }

  error(...args) {
    if (this.enabled) {
      console.error(`[${this.prefix}]`, ...args);
    }
  }

  group(label) {
    if (this.enabled) {
      console.group(`[${this.prefix}] ${label}`);
    }
  }

  groupEnd() {
    if (this.enabled) {
      console.groupEnd();
    }
  }

  table(data) {
    if (this.enabled) {
      console.log(`[${this.prefix}]`);
      console.table(data);
    }
  }
}

const logger = new Logger('MyApp');
logger.log('Application started');

Breakpoints and Debugger

Using the Debugger Statement

function calculateTotal(items) {
  let total = 0;

  for (const item of items) {
    // Pause execution here when DevTools is open
    debugger;

    if (item.price && item.quantity) {
      total += item.price * item.quantity;
    }
  }

  return total;
}

// Conditional debugger
function processData(data) {
  data.forEach((item, index) => {
    // Only break for specific conditions
    if (item.status === 'error') {
      debugger;
    }

    // Process item
    processItem(item);
  });
}

// Debugger in production
function safeDebugger() {
  // Only activate in development
  if (process.env.NODE_ENV === 'development') {
    debugger;
  }
}

Breakpoint Types

// Line breakpoints - Set in DevTools on specific lines

// Conditional breakpoints
function updateUser(user) {
  // Set conditional breakpoint: user.id === 123
  user.lastUpdated = new Date();
  saveUser(user);
}

// DOM breakpoints
// Set in DevTools Elements panel:
// - Subtree modifications
// - Attribute modifications
// - Node removal

// XHR/Fetch breakpoints
// Set in DevTools to break on specific URLs
fetch('/api/users')
  .then((response) => response.json())
  .then((data) => console.log(data));

// Event listener breakpoints
// Set in DevTools to break on specific events
document.addEventListener('click', function (event) {
  console.log('Clicked:', event.target);
});

// Exception breakpoints
function riskyOperation() {
  try {
    // Code that might throw
    JSON.parse('invalid json');
  } catch (error) {
    // DevTools can break on caught/uncaught exceptions
    console.error('Error:', error);
  }
}

Error Handling and Stack Traces

Enhanced Error Information

// Custom error classes
class ValidationError extends Error {
  constructor(field, value, message) {
    super(message);
    this.name = 'ValidationError';
    this.field = field;
    this.value = value;
    this.timestamp = new Date();

    // Capture stack trace
    if (Error.captureStackTrace) {
      Error.captureStackTrace(this, ValidationError);
    }
  }

  toString() {
    return `${this.name} in field '${this.field}': ${this.message}`;
  }
}

// Error context tracking
class ErrorContext {
  constructor() {
    this.context = [];
  }

  push(info) {
    this.context.push({
      ...info,
      timestamp: new Date(),
    });
  }

  pop() {
    return this.context.pop();
  }

  captureError(error) {
    return {
      error,
      context: [...this.context],
      userAgent: navigator.userAgent,
      url: window.location.href,
      timestamp: new Date(),
    };
  }
}

const errorContext = new ErrorContext();

// Usage
function processUser(userId) {
  errorContext.push({ operation: 'processUser', userId });

  try {
    // Operations that might fail
    const user = fetchUser(userId);
    validateUser(user);
    saveUser(user);
  } catch (error) {
    const enhancedError = errorContext.captureError(error);
    console.error('Enhanced error:', enhancedError);
    throw error;
  } finally {
    errorContext.pop();
  }
}

Stack Trace Analysis

// Parse stack traces
function parseStackTrace(error) {
  const stack = error.stack || '';
  const lines = stack.split('\n');

  return lines
    .slice(1)
    .map((line) => {
      const match = line.match(/at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/);
      if (match) {
        return {
          function: match[1],
          file: match[2],
          line: parseInt(match[3]),
          column: parseInt(match[4]),
        };
      }
      return null;
    })
    .filter(Boolean);
}

// Async stack traces
async function asyncOperation() {
  try {
    await step1();
    await step2();
    await step3();
  } catch (error) {
    // Enhanced async stack trace in modern browsers
    console.error('Async error:', error);
    console.log('Stack:', parseStackTrace(error));
  }
}

// Error boundary pattern
class ErrorBoundary {
  constructor(handler) {
    this.handler = handler;
    this.setupGlobalHandlers();
  }

  setupGlobalHandlers() {
    // Catch unhandled errors
    window.addEventListener('error', (event) => {
      this.handler({
        type: 'error',
        error: event.error,
        message: event.message,
        filename: event.filename,
        lineno: event.lineno,
        colno: event.colno,
      });
    });

    // Catch unhandled promise rejections
    window.addEventListener('unhandledrejection', (event) => {
      this.handler({
        type: 'unhandledRejection',
        reason: event.reason,
        promise: event.promise,
      });
    });
  }

  wrap(fn) {
    return (...args) => {
      try {
        const result = fn(...args);
        if (result instanceof Promise) {
          return result.catch((error) => {
            this.handler({ type: 'promise', error });
            throw error;
          });
        }
        return result;
      } catch (error) {
        this.handler({ type: 'sync', error });
        throw error;
      }
    };
  }
}

Performance Debugging

Performance Profiling

// Performance marks and measures
performance.mark('myFunction-start');

// Function to profile
function expensiveOperation() {
  // Simulate expensive operation
  const arr = [];
  for (let i = 0; i < 1000000; i++) {
    arr.push(Math.random());
  }
  return arr.sort();
}

const result = expensiveOperation();

performance.mark('myFunction-end');
performance.measure('myFunction', 'myFunction-start', 'myFunction-end');

// Get performance entries
const measures = performance.getEntriesByName('myFunction');
console.log('Execution time:', measures[0].duration);

// Performance observer
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log(`${entry.name}: ${entry.duration}ms`);
  }
});

observer.observe({ entryTypes: ['measure'] });

// Memory profiling
class MemoryProfiler {
  static snapshot() {
    if (performance.memory) {
      return {
        usedJSHeapSize: performance.memory.usedJSHeapSize,
        totalJSHeapSize: performance.memory.totalJSHeapSize,
        jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
      };
    }
    return null;
  }

  static profile(fn, label = 'Memory Profile') {
    const before = this.snapshot();
    const startTime = performance.now();

    const result = fn();

    const endTime = performance.now();
    const after = this.snapshot();

    if (before && after) {
      console.log(`${label}:`, {
        duration: endTime - startTime,
        memoryDelta: after.usedJSHeapSize - before.usedJSHeapSize,
        before,
        after,
      });
    }

    return result;
  }
}

Performance Monitoring

// FPS Monitor
class FPSMonitor {
  constructor() {
    this.fps = 0;
    this.frames = 0;
    this.lastTime = performance.now();
    this.history = [];
    this.maxHistory = 60;
  }

  start() {
    this.update();
  }

  update() {
    this.frames++;

    const currentTime = performance.now();
    const delta = currentTime - this.lastTime;

    if (delta >= 1000) {
      this.fps = Math.round((this.frames * 1000) / delta);
      this.frames = 0;
      this.lastTime = currentTime;

      this.history.push(this.fps);
      if (this.history.length > this.maxHistory) {
        this.history.shift();
      }

      this.onUpdate(this.fps);
    }

    requestAnimationFrame(() => this.update());
  }

  onUpdate(fps) {
    console.log(`FPS: ${fps}`);
  }

  getAverage() {
    if (this.history.length === 0) return 0;
    const sum = this.history.reduce((a, b) => a + b, 0);
    return Math.round(sum / this.history.length);
  }
}

// Network performance monitoring
class NetworkMonitor {
  static getNetworkInfo() {
    if ('connection' in navigator) {
      const connection = navigator.connection;
      return {
        effectiveType: connection.effectiveType,
        downlink: connection.downlink,
        rtt: connection.rtt,
        saveData: connection.saveData,
      };
    }
    return null;
  }

  static measureFetch(url) {
    const startTime = performance.now();

    return fetch(url).then((response) => {
      const endTime = performance.now();
      const duration = endTime - startTime;

      return {
        response,
        metrics: {
          duration,
          size: response.headers.get('content-length'),
          type: response.headers.get('content-type'),
          status: response.status,
        },
      };
    });
  }
}

Debugging Techniques

Binary Search Debugging

// Finding bugs using binary search
class BinaryDebugger {
  static findBuggyElement(elements, testFunction) {
    let left = 0;
    let right = elements.length - 1;
    let lastGood = -1;

    while (left <= right) {
      const mid = Math.floor((left + right) / 2);
      const testElements = elements.slice(0, mid + 1);

      if (testFunction(testElements)) {
        // Bug not present, search right half
        lastGood = mid;
        left = mid + 1;
      } else {
        // Bug present, search left half
        right = mid - 1;
      }
    }

    return elements[lastGood + 1]; // First buggy element
  }

  static async findBuggyCommit(commits, testFunction) {
    let left = 0;
    let right = commits.length - 1;

    while (left < right) {
      const mid = Math.floor((left + right) / 2);

      if (await testFunction(commits[mid])) {
        left = mid + 1;
      } else {
        right = mid;
      }
    }

    return commits[left];
  }
}

State Debugging

// State snapshot and comparison
class StateDebugger {
  constructor() {
    this.snapshots = [];
    this.maxSnapshots = 10;
  }

  capture(label, state) {
    const snapshot = {
      label,
      timestamp: new Date(),
      state: this.deepClone(state),
      stackTrace: this.getStackTrace(),
    };

    this.snapshots.push(snapshot);

    if (this.snapshots.length > this.maxSnapshots) {
      this.snapshots.shift();
    }

    return snapshot;
  }

  deepClone(obj) {
    return JSON.parse(JSON.stringify(obj));
  }

  getStackTrace() {
    const stack = new Error().stack;
    return stack.split('\n').slice(3, 8).join('\n');
  }

  compare(index1, index2) {
    const snap1 = this.snapshots[index1];
    const snap2 = this.snapshots[index2];

    if (!snap1 || !snap2) {
      throw new Error('Invalid snapshot indices');
    }

    return this.diff(snap1.state, snap2.state);
  }

  diff(obj1, obj2, path = '') {
    const differences = [];

    // Check all keys in obj1
    for (const key in obj1) {
      const newPath = path ? `${path}.${key}` : key;

      if (!(key in obj2)) {
        differences.push({
          path: newPath,
          type: 'deleted',
          oldValue: obj1[key],
        });
      } else if (typeof obj1[key] !== typeof obj2[key]) {
        differences.push({
          path: newPath,
          type: 'changed',
          oldValue: obj1[key],
          newValue: obj2[key],
        });
      } else if (typeof obj1[key] === 'object' && obj1[key] !== null) {
        differences.push(...this.diff(obj1[key], obj2[key], newPath));
      } else if (obj1[key] !== obj2[key]) {
        differences.push({
          path: newPath,
          type: 'changed',
          oldValue: obj1[key],
          newValue: obj2[key],
        });
      }
    }

    // Check for new keys in obj2
    for (const key in obj2) {
      if (!(key in obj1)) {
        const newPath = path ? `${path}.${key}` : key;
        differences.push({ path: newPath, type: 'added', newValue: obj2[key] });
      }
    }

    return differences;
  }

  replay() {
    this.snapshots.forEach((snapshot, index) => {
      console.group(`Snapshot ${index}: ${snapshot.label}`);
      console.log('Time:', snapshot.timestamp);
      console.log('State:', snapshot.state);
      console.log('Stack:', snapshot.stackTrace);
      console.groupEnd();
    });
  }
}

Remote Debugging

// Remote debugging helper
class RemoteDebugger {
  constructor(endpoint) {
    this.endpoint = endpoint;
    this.sessionId = this.generateSessionId();
    this.buffer = [];
    this.maxBufferSize = 100;
    this.flushInterval = 5000;

    this.startFlushTimer();
    this.interceptConsole();
  }

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

  interceptConsole() {
    const methods = ['log', 'warn', 'error', 'info'];

    methods.forEach((method) => {
      const original = console[method];
      console[method] = (...args) => {
        original.apply(console, args);
        this.capture(method, args);
      };
    });
  }

  capture(level, args) {
    const entry = {
      level,
      message: args.map((arg) => this.serialize(arg)).join(' '),
      timestamp: new Date().toISOString(),
      url: window.location.href,
      userAgent: navigator.userAgent,
      stack: new Error().stack,
    };

    this.buffer.push(entry);

    if (this.buffer.length >= this.maxBufferSize) {
      this.flush();
    }
  }

  serialize(obj) {
    try {
      if (typeof obj === 'object') {
        return JSON.stringify(obj, null, 2);
      }
      return String(obj);
    } catch (e) {
      return '[Unserializable]';
    }
  }

  async flush() {
    if (this.buffer.length === 0) return;

    const data = {
      sessionId: this.sessionId,
      entries: [...this.buffer],
    };

    this.buffer = [];

    try {
      await fetch(this.endpoint, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data),
      });
    } catch (error) {
      console.error('Failed to send debug data:', error);
    }
  }

  startFlushTimer() {
    setInterval(() => this.flush(), this.flushInterval);
  }
}

Visual Debugging

// DOM element highlighting
class VisualDebugger {
  static highlight(element, color = 'red', duration = 2000) {
    const originalBorder = element.style.border;
    element.style.border = `3px solid ${color}`;

    setTimeout(() => {
      element.style.border = originalBorder;
    }, duration);
  }

  static showBoundingBox(element) {
    const rect = element.getBoundingClientRect();
    const box = document.createElement('div');

    Object.assign(box.style, {
      position: 'fixed',
      top: rect.top + 'px',
      left: rect.left + 'px',
      width: rect.width + 'px',
      height: rect.height + 'px',
      border: '2px solid red',
      backgroundColor: 'rgba(255, 0, 0, 0.1)',
      pointerEvents: 'none',
      zIndex: 9999,
    });

    document.body.appendChild(box);

    setTimeout(() => box.remove(), 3000);
  }

  static visualizeEventFlow(element) {
    const events = ['click', 'mouseenter', 'mouseleave', 'focus', 'blur'];
    const log = document.createElement('div');

    Object.assign(log.style, {
      position: 'fixed',
      top: '10px',
      right: '10px',
      width: '300px',
      maxHeight: '400px',
      overflow: 'auto',
      backgroundColor: 'rgba(0, 0, 0, 0.8)',
      color: 'white',
      padding: '10px',
      fontSize: '12px',
      fontFamily: 'monospace',
      zIndex: 10000,
    });

    document.body.appendChild(log);

    events.forEach((eventType) => {
      element.addEventListener(
        eventType,
        function (event) {
          const entry = document.createElement('div');
          entry.textContent = `${new Date().toLocaleTimeString()}: ${eventType} on ${event.target.tagName}`;
          log.appendChild(entry);
          log.scrollTop = log.scrollHeight;
        },
        true
      );
    });

    return () => log.remove();
  }
}

// Layout debugging
class LayoutDebugger {
  static showAllBorders() {
    const style = document.createElement('style');
    style.textContent = `
      * { outline: 1px solid red !important; }
      *:before { outline: 1px solid green !important; }
      *:after { outline: 1px solid blue !important; }
    `;
    document.head.appendChild(style);

    return () => style.remove();
  }

  static showGridOverlay() {
    const overlay = document.createElement('div');

    Object.assign(overlay.style, {
      position: 'fixed',
      top: 0,
      left: 0,
      right: 0,
      bottom: 0,
      backgroundImage: `
        repeating-linear-gradient(
          0deg,
          rgba(0, 0, 0, 0.1) 0px,
          transparent 1px,
          transparent 10px,
          rgba(0, 0, 0, 0.1) 11px
        ),
        repeating-linear-gradient(
          90deg,
          rgba(0, 0, 0, 0.1) 0px,
          transparent 1px,
          transparent 10px,
          rgba(0, 0, 0, 0.1) 11px
        )
      `,
      pointerEvents: 'none',
      zIndex: 9998,
    });

    document.body.appendChild(overlay);

    return () => overlay.remove();
  }
}

Testing and Debugging Integration

// Debug test helper
class TestDebugger {
  static async runWithDebug(testFn, options = {}) {
    const debug = {
      logs: [],
      errors: [],
      snapshots: [],
      performance: [],
    };

    // Intercept console
    const originalLog = console.log;
    const originalError = console.error;

    console.log = (...args) => {
      debug.logs.push({ timestamp: Date.now(), args });
      if (options.showLogs) originalLog(...args);
    };

    console.error = (...args) => {
      debug.errors.push({ timestamp: Date.now(), args });
      if (options.showErrors) originalError(...args);
    };

    // Run test
    const startTime = performance.now();
    let result;
    let error;

    try {
      result = await testFn();
    } catch (e) {
      error = e;
    }

    const endTime = performance.now();

    // Restore console
    console.log = originalLog;
    console.error = originalError;

    // Create report
    const report = {
      success: !error,
      result,
      error,
      duration: endTime - startTime,
      debug,
    };

    if (options.verbose) {
      this.printReport(report);
    }

    return report;
  }

  static printReport(report) {
    console.group('Test Debug Report');
    console.log('Success:', report.success);
    console.log('Duration:', report.duration + 'ms');

    if (report.error) {
      console.group('Error');
      console.error(report.error);
      console.groupEnd();
    }

    if (report.debug.logs.length > 0) {
      console.group('Logs');
      report.debug.logs.forEach((log) => {
        console.log(...log.args);
      });
      console.groupEnd();
    }

    console.groupEnd();
  }
}

Best Practices

  1. Use appropriate debugging tools

    // Development vs Production
    const debug =
      process.env.NODE_ENV === 'development' ? console.log : () => {};
    
  2. Clean up debug code

    // Use debug flags
    const DEBUG = false;
    if (DEBUG) {
      console.log('Debug info');
    }
    
  3. Structured logging

    function log(level, message, data = {}) {
      console.log(
        JSON.stringify({
          level,
          message,
          data,
          timestamp: new Date().toISOString(),
        })
      );
    }
    
  4. Preserve error context

    try {
      riskyOperation();
    } catch (error) {
      error.context = { user: currentUser, action: 'riskyOperation' };
      throw error;
    }
    

Conclusion

Effective debugging is crucial for JavaScript development:

  • Browser DevTools for interactive debugging
  • Console methods for logging and inspection
  • Breakpoints for step-by-step execution
  • Performance profiling for optimization
  • Error tracking for production issues
  • Visual debugging for UI problems

Key takeaways:

  • Master browser developer tools
  • Use appropriate debugging techniques
  • Implement proper error handling
  • Monitor performance metrics
  • Clean up debug code before production
  • Document debugging findings

Master debugging to efficiently solve problems and build robust applications!