Mastering JavaScript Debugging — Chrome DevTools Practical Guide

Getting the Most Out of Chrome DevTools

Debugging with console.log is the most basic approach, but it has limitations when tracking down complex bugs. Chrome DevTools provides powerful debugging tools including breakpoints, network analysis, memory profiling, and performance measurement. This article covers the essential DevTools features with practical examples.

Using the console API Properly

There are many more methods beyond console.log. Using the right method for each situation greatly improves debugging efficiency.

// console.table — display arrays/objects in table format
const users = [
  { name: "Alice", role: "admin", active: true },
  { name: "Bob", role: "editor", active: false },
  { name: "Charlie", role: "viewer", active: true },
];
console.table(users);
// ┌─────────┬───────────┬──────────┬────────┐
// │ (index) │   name    │   role   │ active │
// ├─────────┼───────────┼──────────┼────────┤
// │    0    │ "Alice"   │ "admin"  │  true  │
// │    1    │ "Bob"     │ "editor" │ false  │
// │    2    │ "Charlie" │ "viewer" │  true  │
// └─────────┴───────────┴──────────┴────────┘

// console.group — group related logs together
console.group("Loading user data");
console.log("API call started");
console.log(`URL: /api/users`);
console.warn("Cache miss — fetching from server");
console.groupEnd();

// console.time — measure execution time
console.time("Data processing");
const processed = users.filter((u) => u.active);
console.timeEnd("Data processing");
// Data processing: 0.012ms

// console.assert — only outputs when condition is false
console.assert(users.length > 0, "User list is empty");
console.assert(users.length > 10, "Less than 10 users"); // This prints

// console.trace — trace the call stack
function innerFunction() {
  console.trace("Called from here");
}
function outerFunction() {
  innerFunction();
}
outerFunction();
// Trace: Called from here
//     at innerFunction (app.js:35)
//     at outerFunction (app.js:38)
MethodUse caseTip
console.table()Display arrays/objects as tablesUseful for checking API response data
console.group()Group logsTrack nested processing flows
console.time()Measure execution timeIdentify performance bottlenecks
console.assert()Conditional outputVerify invariant conditions
console.trace()Print call stackTrace function call paths
console.count()Count invocationsCheck event frequency

Using Breakpoints

Breakpoints are a powerful tool that pauses code execution at specific points and lets you inspect variable states.

// 1. debugger keyword — set breakpoints directly in code
function processOrder(order) {
  const total = order.items.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
  );

  // Only pause when total is negative
  if (total < 0) {
    debugger; // Only works when DevTools is open
  }

  const tax = total * 0.1;
  const grandTotal = total + tax;
  return { total, tax, grandTotal };
}

// 2. Conditional breakpoints (set in DevTools)
// Sources panel -> right-click line number -> "Add conditional breakpoint"
// Example condition: order.total > 100000

// 3. DOM change breakpoints
// Elements panel -> right-click element -> "Break on"
//   - subtree modifications: when child elements change
//   - attribute modifications: when attributes change
//   - node removal: when element is removed

// 4. XHR/Fetch breakpoints
// Sources panel -> XHR/fetch Breakpoints -> add URL pattern
// Example: pause on requests containing "api/users"

// 5. Event listener breakpoints
// Sources panel -> Event Listener Breakpoints
// Example: check Mouse -> click -> pause on click events

Network Panel

Inspect detailed information about network requests, and track slow or failed requests.

// Fetch interceptor — log all network requests
const originalFetch = window.fetch;

window.fetch = async function (...args) {
  const url = typeof args[0] === "string" ? args[0] : args[0].url;
  const method =
    args[1]?.method || "GET";

  console.group(`[Fetch] ${method} ${url}`);
  console.time("Response time");

  try {
    const response = await originalFetch.apply(this, args);

    console.log("Status:", response.status);
    console.log("Headers:", Object.fromEntries(response.headers));
    console.timeEnd("Response time");

    if (!response.ok) {
      console.error(`HTTP error: ${response.status} ${response.statusText}`);
    }

    console.groupEnd();
    return response;
  } catch (error) {
    console.error("Network error:", error.message);
    console.timeEnd("Response time");
    console.groupEnd();
    throw error;
  }
};

// Key Network panel features:
// - Filters: filter by type (XHR, Fetch, JS, CSS, Img, etc.)
// - Throttling: simulate slow networks (Slow 3G, Offline)
// - Waterfall: visualize request timing
// - Preserve log: keep logs across page navigations

Performance Profiling

Use the Performance panel to find rendering bottlenecks and identify causes of frame drops.

// Set custom markers with the Performance API
function measureRender(componentName) {
  performance.mark(`${componentName}-start`);

  return {
    end() {
      performance.mark(`${componentName}-end`);
      performance.measure(
        componentName,
        `${componentName}-start`,
        `${componentName}-end`
      );

      const entries = performance.getEntriesByName(componentName);
      const duration = entries[entries.length - 1].duration;
      console.log(`[Perf] ${componentName}: ${duration.toFixed(2)}ms`);

      // Warn if exceeding 16ms (60fps threshold)
      if (duration > 16) {
        console.warn(
          `[Perf Warning] ${componentName} took ` +
          `${duration.toFixed(0)}ms (exceeds 16ms)`
        );
      }

      return duration;
    },
  };
}

// Usage example
const measure = measureRender("UserListRender");
// ... rendering logic ...
const duration = measure.end();
// [Perf] UserListRender: 23.45ms
// [Perf Warning] UserListRender took 23ms (exceeds 16ms)

// Measuring Web Vitals
function observeWebVitals() {
  // Largest Contentful Paint (LCP)
  new PerformanceObserver((list) => {
    const entries = list.getEntries();
    const lastEntry = entries[entries.length - 1];
    console.log(`LCP: ${lastEntry.startTime.toFixed(0)}ms`);
  }).observe({ type: "largest-contentful-paint", buffered: true });

  // Cumulative Layout Shift (CLS)
  let clsScore = 0;
  new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      if (!entry.hadRecentInput) {
        clsScore += entry.value;
      }
    }
    console.log(`CLS: ${clsScore.toFixed(4)}`);
  }).observe({ type: "layout-shift", buffered: true });
}

Memory Debugging

Memory leaks are common in long-running SPAs. Use DevTools’ Memory panel to detect them.

// Common causes of memory leaks and their solutions

// 1. Unreleased event listeners
class BadComponent {
  constructor() {
    // Leak: listener remains after component is removed
    window.addEventListener("resize", this.handleResize);
  }
  handleResize = () => {
    console.log("resize");
  };
}

class GoodComponent {
  #controller = new AbortController();

  constructor() {
    // AbortController enables bulk listener removal
    window.addEventListener("resize", this.handleResize, {
      signal: this.#controller.signal,
    });
  }

  handleResize = () => {
    console.log("resize");
  };

  destroy() {
    this.#controller.abort(); // Remove all listeners at once
  }
}

// 2. Leaks caused by closures
function createLeakyHandler() {
  const hugeData = new Array(1000000).fill("leak");
  // hugeData is kept alive by the closure
  return () => console.log(hugeData.length);
}

// 3. Leaks caused by DOM references
const detachedNodes = new Map();
function removeElement(id) {
  const element = document.getElementById(id);
  detachedNodes.set(id, element); // Reference kept even after DOM removal
  element.remove();
  // Fix: detachedNodes.delete(id);
}

// Using the Memory panel:
// 1. Take a Heap snapshot -> capture current memory usage
// 2. Perform operations, then take a second snapshot
// 3. Use the Comparison view to see which objects increased
// 4. Check Retainers -> identify what references prevent GC

Useful Shortcuts

// Open DevTools: F12 or Ctrl+Shift+I (Windows)

// Console panel shortcuts
// $_   : result of the last evaluated expression
// $0   : currently selected element in the Elements panel
// $("selector") : shorthand for document.querySelector
// $$("selector") : shorthand for document.querySelectorAll
// copy(obj) : copy an object to the clipboard
// monitor(fn) : log every time a function is called
// monitorEvents(element, "click") : monitor events

// Practical usage examples
// $0.style.border = "2px solid red"  // Add red border to selected element
// copy(JSON.stringify(data, null, 2))  // Copy JSON data to clipboard
// monitor(fetchUser)  // Trace fetchUser calls

Practical Tips

  • Use breakpoints instead of console.log: For complex bugs, stepping through code line by line and inspecting variable states is far more efficient
  • Use Logpoints: Right-click a breakpoint and select “Add logpoint” to output logs without modifying code
  • Blackboxing: Blackbox library files in the Sources panel so you only debug your own code
  • Network conditional filters: Use status-code:500 or larger-than:1M to filter specific request conditions
  • Snippets: Save frequently used debugging code in Sources panel Snippets
  • Performance Monitor: Search for “Performance Monitor” in Ctrl+Shift+P to monitor real-time CPU, heap, and DOM node count
  • Coverage: Search for “Coverage” in Ctrl+Shift+P to identify unused CSS/JS code

Was this article helpful?