Debugging JavaScript

45 min read Intermediate

Master debugging techniques using browser DevTools, console methods, breakpoints, and systematic debugging strategies.

Console Methods

The console object provides various methods beyond console.log().

JavaScript
// Basic logging
console.log("Simple message");
console.log("Value:", variable, "Another:", other);

// Log levels
console.info("Informational message");
console.warn("Warning message");   // Yellow highlight
console.error("Error message");    // Red highlight, with stack trace

// Format output
console.log("%c Styled text", "color: blue; font-size: 20px");
console.log("%o", object);  // Object format
console.log("%d", 123);     // Integer
console.log("%f", 3.14);    // Float
console.log("%s", "text");  // String

// Display objects
const user = { name: "John", age: 30 };
console.log(user);          // Collapsed view
console.dir(user);          // Full object tree
console.table([             // Table format
    { name: "John", age: 30 },
    { name: "Jane", age: 25 }
]);

// Grouping
console.group("User Details");
console.log("Name:", user.name);
console.log("Age:", user.age);
console.groupEnd();

console.groupCollapsed("Hidden by default");
console.log("Details inside");
console.groupEnd();

// Assertions
console.assert(1 === 1, "This won't show");
console.assert(1 === 2, "Assertion failed!"); // Shows error

// Counting
console.count("loop");  // loop: 1
console.count("loop");  // loop: 2
console.countReset("loop");

// Timing
console.time("operation");
// ... code to measure ...
console.timeLog("operation");  // Intermediate time
// ... more code ...
console.timeEnd("operation");  // Final time

// Stack trace
function a() { b(); }
function b() { c(); }
function c() { console.trace("Call stack:"); }
a(); // Shows: c -> b -> a

// Clear console
console.clear();

Breakpoints & DevTools

Use browser DevTools for step-by-step debugging.

JavaScript
// Programmatic breakpoint
function calculateTotal(items) {
    debugger; // Execution pauses here when DevTools is open
    
    let total = 0;
    for (const item of items) {
        total += item.price * item.quantity;
    }
    return total;
}

// Conditional breakpoint (set in DevTools)
// Right-click line number -> "Add conditional breakpoint"
// Example condition: item.price > 100


// ===== DevTools Features =====

// Sources Panel:
// - Set breakpoints by clicking line numbers
// - Step Over (F10) - next line
// - Step Into (F11) - enter function
// - Step Out (Shift+F11) - exit function
// - Continue (F8) - run until next breakpoint

// Watch expressions:
// Add variables to watch panel to monitor their values

// Call Stack:
// Shows the path of function calls to current point

// Scope:
// View local, closure, and global variables


// ===== Event Listener Breakpoints =====
// In Sources panel, expand "Event Listener Breakpoints"
// Check events like "Mouse > click" to break on any click handler


// ===== XHR/Fetch Breakpoints =====
// Break when URL contains specific string
// Useful for debugging API calls


// ===== DOM Breakpoints =====
// Right-click element in Elements panel
// - Break on subtree modifications
// - Break on attribute modifications
// - Break on node removal
DevTools Keyboard Shortcuts
  • F12 or Ctrl+Shift+I - Open DevTools
  • Ctrl+Shift+J - Open Console directly
  • Ctrl+P - Quick file open in Sources
  • Ctrl+Shift+F - Search all files

Debugging Errors

Techniques for tracking down and fixing common errors.

JavaScript
// ===== TypeError =====
// "Cannot read property 'x' of undefined"
const obj = undefined;
// obj.property; // Error!

// Fix: Check before accessing
if (obj && obj.property) { }
// Or use optional chaining
obj?.property;


// ===== ReferenceError =====
// "x is not defined"
// console.log(undefinedVar); // Error!

// Fix: Ensure variable is declared
let definedVar = "value";


// ===== SyntaxError =====
// Usually caught before runtime
// Check for:
// - Missing brackets/parentheses
// - Invalid JSON
// - Reserved words as identifiers


// ===== Debugging async code =====
async function fetchData() {
    try {
        const response = await fetch("/api/data");
        console.log("Response status:", response.status);
        
        if (!response.ok) {
            console.error("Request failed:", response.statusText);
            return;
        }
        
        const data = await response.json();
        console.log("Data received:", data);
        return data;
        
    } catch (error) {
        console.error("Fetch error:", error);
        console.error("Stack:", error.stack);
    }
}


// ===== Debugging event listeners =====
function handleClick(event) {
    console.log("Event type:", event.type);
    console.log("Target:", event.target);
    console.log("Current target:", event.currentTarget);
    console.log("Event phase:", event.eventPhase);
    // Do something
}


// ===== Debugging this context =====
const obj2 = {
    value: 42,
    getValue() {
        console.log("this is:", this);
        console.log("this.value:", this.value);
        return this.value;
    }
};

obj2.getValue();  // this = obj2
const fn = obj2.getValue;
fn();  // this = undefined (strict) or window

// Fix: bind, arrow function, or call with context
fn.call(obj2);  // this = obj2

Debugging Strategies

Systematic approaches to finding and fixing bugs.

JavaScript
// ===== 1. Reproduce the Bug =====
// Create minimal test case that triggers the bug
function buggyFunction(data) {
    // Simplify until you isolate the issue
}

// ===== 2. Binary Search Debugging =====
// Add console.log in the middle of code
// If bug is before that point, move log earlier
// If after, move log later
function processData(items) {
    console.log("Step 1: items received", items);
    
    const filtered = items.filter(x => x.active);
    console.log("Step 2: after filter", filtered);
    
    const mapped = filtered.map(x => x.value);
    console.log("Step 3: after map", mapped);
    
    return mapped.reduce((a, b) => a + b, 0);
}


// ===== 3. Rubber Duck Debugging =====
// Explain your code line-by-line out loud
// Often reveals faulty assumptions


// ===== 4. Check Inputs and Outputs =====
function calculate(a, b) {
    console.log("Input:", { a, b, typeA: typeof a, typeB: typeof b });
    
    const result = a + b;
    console.log("Output:", result, typeof result);
    
    return result;
}

calculate(5, "3");  // Might reveal string concatenation!


// ===== 5. Use Defensive Programming =====
function divide(a, b) {
    if (typeof a !== "number" || typeof b !== "number") {
        throw new TypeError("Arguments must be numbers");
    }
    if (b === 0) {
        throw new Error("Cannot divide by zero");
    }
    return a / b;
}


// ===== 6. Isolate Components =====
// Test each function independently
function validateEmail(email) {
    const pattern = /^[\w.-]+@[\w.-]+\.\w{2,}$/;
    return pattern.test(email);
}

// Test in console
console.log(validateEmail("test@example.com")); // true
console.log(validateEmail("invalid"));          // false


// ===== 7. Check for Common Issues =====
// - Off-by-one errors in loops
// - Async timing issues
// - Scope and closure problems
// - Mutating objects unexpectedly
// - Type coercion surprises

Network Debugging

Debug API calls and network issues.

JavaScript
// Wrap fetch for logging
async function debugFetch(url, options = {}) {
    console.group(`Fetch: ${options.method || "GET"} ${url}`);
    console.log("Options:", options);
    console.time("Request duration");
    
    try {
        const response = await fetch(url, options);
        
        console.log("Status:", response.status, response.statusText);
        console.log("Headers:", Object.fromEntries(response.headers));
        
        // Clone to read body without consuming it
        const clone = response.clone();
        const body = await clone.text();
        console.log("Body:", body.substring(0, 500));
        
        console.timeEnd("Request duration");
        console.groupEnd();
        
        return response;
        
    } catch (error) {
        console.error("Request failed:", error);
        console.timeEnd("Request duration");
        console.groupEnd();
        throw error;
    }
}

// DevTools Network Panel:
// - View all requests and responses
// - Filter by type (XHR, Fetch, JS, CSS)
// - See request/response headers
// - Preview response body
// - View timing breakdown
// - Throttle connection speed
// - Block specific requests
// - Replay requests

Performance Profiling

Chrome DevTools provides powerful tools to identify performance bottlenecks, analyze runtime behavior, and optimize your JavaScript code.

Performance Panel

JavaScript - Performance Measurement
// Mark timeline for Performance panel
performance.mark('startTask');

// Do expensive work
processLargeDataset(data);

performance.mark('endTask');
performance.measure('processData', 'startTask', 'endTask');

// Get measurements
const measures = performance.getEntriesByName('processData');
console.log('Duration:', measures[0].duration, 'ms');

// Clean up
performance.clearMarks();
performance.clearMeasures();

// User Timing API for custom metrics
class PerformanceTracker {
    static startMeasure(name) {
        performance.mark(`${name}-start`);
    }
    
    static endMeasure(name) {
        performance.mark(`${name}-end`);
        performance.measure(name, `${name}-start`, `${name}-end`);
        
        const entry = performance.getEntriesByName(name)[0];
        console.log(`[Performance] ${name}: ${entry.duration.toFixed(2)}ms`);
        
        // Send to analytics
        if (entry.duration > 100) {
            analytics.track('slow_operation', { name, duration: entry.duration });
        }
        
        return entry.duration;
    }
}

// Usage
PerformanceTracker.startMeasure('render');
renderComponent();
PerformanceTracker.endMeasure('render');

// Frame timing for animations
function measureFPS() {
    let frames = 0;
    let lastTime = performance.now();
    
    function count() {
        frames++;
        const now = performance.now();
        
        if (now - lastTime >= 1000) {
            console.log('FPS:', frames);
            frames = 0;
            lastTime = now;
        }
        
        requestAnimationFrame(count);
    }
    
    requestAnimationFrame(count);
}

Memory Leaks & Profiling

Memory leaks cause applications to slow down over time. Learn to identify and fix common memory issues using DevTools Memory panel.

Common Memory Leak Patterns

JavaScript - Memory Leak Patterns
// ❌ Leak 1: Forgotten event listeners
class LeakyComponent {
    constructor() {
        this.handleScroll = () => this.onScroll();
        window.addEventListener('scroll', this.handleScroll);
    }
    
    onScroll() { /* ... */ }
    
    destroy() {
        // Forgot to remove listener!
    }
}

// ✅ Fix: Always clean up
class FixedComponent {
    constructor() {
        this.handleScroll = this.onScroll.bind(this);
        window.addEventListener('scroll', this.handleScroll);
    }
    
    onScroll() { /* ... */ }
    
    destroy() {
        window.removeEventListener('scroll', this.handleScroll);
    }
}


// ❌ Leak 2: Closures holding references
function createHandler(element) {
    const hugeData = new Array(1000000).fill('data');
    
    return function handler() {
        console.log(element.id);  // Holds reference to element AND hugeData
    };
}

// ✅ Fix: Only capture what you need
function createHandlerFixed(element) {
    const id = element.id;  // Capture only the id
    
    return function handler() {
        console.log(id);
    };
}


// ❌ Leak 3: Growing collections
const cache = {};
function processItem(item) {
    cache[item.id] = expensiveComputation(item);  // Never cleared!
    return cache[item.id];
}

// ✅ Fix: Use WeakMap or LRU cache
const cache = new WeakMap();  // Auto-cleanup when key is GC'd

// Or implement LRU cache
class LRUCache {
    constructor(maxSize = 100) {
        this.cache = new Map();
        this.maxSize = maxSize;
    }
    
    get(key) {
        if (this.cache.has(key)) {
            const value = this.cache.get(key);
            this.cache.delete(key);
            this.cache.set(key, value);  // Move to end
            return value;
        }
        return undefined;
    }
    
    set(key, value) {
        if (this.cache.size >= this.maxSize) {
            const oldest = this.cache.keys().next().value;
            this.cache.delete(oldest);
        }
        this.cache.set(key, value);
    }
}


// ❌ Leak 4: Timers not cleared
function startPolling() {
    setInterval(() => {
        fetchData();  // Runs forever even if component unmounts
    }, 1000);
}

// ✅ Fix: Clear timers
function startPollingFixed() {
    const intervalId = setInterval(() => {
        fetchData();
    }, 1000);
    
    return () => clearInterval(intervalId);  // Return cleanup function
}

Using Memory DevTools

JavaScript - Memory Debugging Tips
// Memory Panel workflow:

// 1. Take Heap Snapshot
//    - DevTools > Memory > Heap Snapshot > Take Snapshot
//    - Shows all objects in memory

// 2. Record Allocation Timeline
//    - DevTools > Memory > Allocation instrumentation on timeline
//    - Shows memory allocation over time
//    - Blue bars = allocated, gray = freed

// 3. Compare Snapshots
//    - Take snapshot before action
//    - Perform suspected leaky action
//    - Take another snapshot
//    - Compare to see what wasn't released

// Programmatic memory info (Chrome)
if (performance.memory) {
    console.log('JS Heap Size Limit:', performance.memory.jsHeapSizeLimit);
    console.log('Total JS Heap Size:', performance.memory.totalJSHeapSize);
    console.log('Used JS Heap Size:', performance.memory.usedJSHeapSize);
}

// Force garbage collection (DevTools only)
// 1. Open DevTools
// 2. Click "Collect Garbage" button in Memory panel
// Or use: gc() in console (with --expose-gc flag)

// Memory-efficient patterns
// Use object pooling for frequently created objects
class ObjectPool {
    constructor(factory, reset) {
        this.factory = factory;
        this.reset = reset;
        this.pool = [];
    }
    
    acquire() {
        return this.pool.pop() || this.factory();
    }
    
    release(obj) {
        this.reset(obj);
        this.pool.push(obj);
    }
}

const particlePool = new ObjectPool(
    () => ({ x: 0, y: 0, vx: 0, vy: 0 }),
    (p) => { p.x = p.y = p.vx = p.vy = 0; }
);
Memory Leak Warning Signs
  • Growing memory: Memory usage increases over time without plateauing
  • Slow performance: App gets slower the longer it runs
  • Detached DOM nodes: Elements removed from DOM but still referenced
  • Browser crashes: Tab runs out of memory on long-running pages

Summary

Console Methods

log, warn, error, table, group, time

Breakpoints

debugger keyword, line/conditional

DevTools

Sources, Network, Elements panels

Step Debugging

Step over, into, out, continue

Error Analysis

Read stack traces, check types

Strategies

Binary search, isolate, rubber duck