Skip to content
JavaScript js tooling 5 min read

Debugging with DevTools

console.log will only take you so far. The moment a bug depends on runtime state — a value that is undefined two function calls deep, a request that fires twice, a loop that runs one time too many — you need to pause execution and look around. Browser DevTools (and the matching Node inspector) let you freeze your program mid-flight, inspect every variable in scope, walk the call stack, and measure exactly where time goes. This page covers the workflow that turns guessing into observation.

Opening the debugger

In Chrome, Edge, or any Chromium browser, press F12 (or Cmd+Option+I on macOS) and switch to the Sources panel. Firefox uses the Debugger panel; the concepts map one-to-one. For Node, run with the inspector flag and open chrome://inspect:

node --inspect-brk app.js

--inspect-brk pauses on the first line so you can set breakpoints before anything runs; plain --inspect starts immediately.

Setting breakpoints

A breakpoint tells the engine to halt right before a line executes. Click the line number gutter in Sources to add one. You can also pause from code with the debugger statement, which the engine honours whenever DevTools is open:

function applyDiscount(price, code) {
  const rate = lookupRate(code);
  debugger; // execution stops here when DevTools is open
  return price * (1 - rate);
}

DevTools supports several breakpoint types beyond the plain line breakpoint:

TypeHow to setWhen to use
LineClick the gutterPause every time a line runs
ConditionalRight-click gutter → Add conditional breakpointPause only when an expression is truthy
LogpointRight-click gutter → Add logpointPrint a value without editing source
DOMElements panel → right-click nodePause when an element changes
Event listenerSources → Event Listener BreakpointsPause on click, fetch, etc.
XHR / fetchSources → XHR/fetch BreakpointsPause when a URL is requested

Conditional breakpoints and logpoints

Conditional breakpoints are invaluable inside loops. Instead of clicking Resume hundreds of times, give a condition like item.id === 42 and the loop only stops on the iteration that matters.

Logpoints are even lighter: they log an expression and keep running, so you get console.log-style output without touching the file or triggering a rebuild. Enter an expression such as `user ${user.id}: ${user.role}` and DevTools prints it each time the line executes.

Logpoints survive a page reload but live only in DevTools — they never appear in your committed code, so there are no stray console.log calls to clean up later.

Stepping through code

Once paused, the stepping controls drive execution one piece at a time:

ControlShortcutBehaviour
ResumeF8Run until the next breakpoint
Step overF10Run the current line, skip into calls
Step intoF11Enter the function being called
Step outShift+F11Finish the current function and return

Use Step over to move line by line through the function you care about, and Step into only when you need to descend into a callee. Step out quickly escapes a helper you stepped into by accident.

Call stack and scope

While paused, the Call Stack pane shows the chain of function calls that led here — the topmost frame is where you are, and each frame below is the caller. Clicking a frame moves the inspection context to that point, updating the Scope pane to show that frame’s local, closure, and global variables.

const getOrder = async (id) => {
  const res = await fetch(`/api/orders/${id}`);
  return res.json(); // breakpoint here
};

const renderOrder = async (id) => {
  const order = await getOrder(id); // caller frame visible in stack
  document.querySelector("#total").textContent = order.total;
};

The Scope pane lets you read — and edit — values live. Double-click a variable to change it and watch how the rest of execution responds. To cut noise from library frames, right-click a file and choose Add script to ignore list (formerly “blackbox”); the debugger then steps straight past it.

Watch expressions

The Watch pane evaluates expressions on every pause, so you can monitor a value as it changes across breakpoints. Add something like cart.items.length or state.user?.role ?? "guest" and it re-evaluates each time execution halts. The Console drawer (toggle with Esc) runs against the currently selected stack frame, so any expression you type there sees the same in-scope variables the debugger does.

The Network panel

The Network panel records every request the page makes. Each row exposes the method, status, type, size, and timing; clicking one opens tabs for Headers, Payload, Response, and Timing. Common workflow tips:

  • Enable Preserve log to keep requests across navigations and redirects.
  • Use Disable cache (while DevTools is open) to test cold loads.
  • Filter by Fetch/XHR to isolate API calls from images and scripts.
  • Throttle to Slow 3G to reproduce timeout and loading-state bugs.
  • Right-click a request → Copy as fetch to replay it from the Console.

The Timing tab breaks a request into queuing, TTFB (waiting), and content download, which tells you whether slowness is the network or your server.

The Performance panel

When the issue is “the page feels janky,” the Performance panel is the right tool. Click record, interact with the page, then stop. You get a flame chart of the main thread: long yellow blocks are scripting, purple is layout/rendering. Tasks longer than 50 ms are flagged as long tasks because they block input.

// Wrap suspect work in performance marks to find it fast in the timeline.
performance.mark("filter-start");
const visible = products.filter((p) => p.inStock && p.price < budget);
performance.mark("filter-end");
performance.measure("filter", "filter-start", "filter-end");
console.log(performance.getEntriesByName("filter")[0].duration);

Output:

3.7000000178813934

The measure shows up as a labelled band in the Timings track of the recording, letting you correlate your own markers with the flame chart.

Best Practices

  • Reach for breakpoints before console.log — they show full scope without editing source.
  • Use conditional breakpoints and logpoints inside loops to avoid resume-spamming.
  • Ignore-list framework and vendor files so stepping stays inside your own code.
  • Keep Preserve log and Disable cache on while actively debugging network issues.
  • Profile with the Performance panel rather than guessing which code is slow.
  • Add performance.mark/measure calls to label your own work in the timeline.
  • Remove stray debugger statements before committing — a lint rule can catch them.
Last updated June 1, 2026
Was this helpful?