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:
| Type | How to set | When to use |
|---|---|---|
| Line | Click the gutter | Pause every time a line runs |
| Conditional | Right-click gutter → Add conditional breakpoint | Pause only when an expression is truthy |
| Logpoint | Right-click gutter → Add logpoint | Print a value without editing source |
| DOM | Elements panel → right-click node | Pause when an element changes |
| Event listener | Sources → Event Listener Breakpoints | Pause on click, fetch, etc. |
| XHR / fetch | Sources → XHR/fetch Breakpoints | Pause 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.logcalls to clean up later.
Stepping through code
Once paused, the stepping controls drive execution one piece at a time:
| Control | Shortcut | Behaviour |
|---|---|---|
| Resume | F8 | Run until the next breakpoint |
| Step over | F10 | Run the current line, skip into calls |
| Step into | F11 | Enter the function being called |
| Step out | Shift+F11 | Finish 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/XHRto 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/measurecalls to label your own work in the timeline. - Remove stray
debuggerstatements before committing — a lint rule can catch them.