Debugging in the browser

Sooner or later a script will do something you didn’t expect. A value comes out wrong, a function never runs, a loop stops one step early. You could stare at the code until the mistake reveals itself. There’s a faster way: freeze the program mid-run and look at it.

That’s debugging — locating and fixing the errors in a script. Every modern browser ships a debugger built into its developer tools: a UI that lets you pause execution, inspect variables at that exact instant, and walk through the code one statement at a time. It turns “why is this broken?” from guesswork into observation.

The examples below use Chrome, since its tools are thorough and widely available. Firefox, Edge, and Safari offer nearly identical features under slightly different names, so the skills transfer.

The “Sources” panel

Your Chrome version may look slightly different from the screenshots, but the layout is stable enough that you’ll recognize each piece.

  • Open an example page in Chrome — a page that loads a player.js script.
  • Open developer tools with F12 (Mac: Cmd+Opt+I).
  • Click the Sources panel.

Here’s roughly what a first visit looks like:

ElementsConsoleSourcesNetwork»filesSelect a file to view its sourcethe editor and debug panes are empty until then
The Sources panel when you first open it — tabs across the top, the Sources tab active, an empty editor waiting for a file.

There’s a toggle button that opens the pane listing the page’s files. Click it, then pick player.js in the tree. The panel fills out into three regions:

1 · Filesindex.htmlplayer.jsstyle.csslogo.png2 · Code12343 · DebugWatchCall StackScopeBreakpoints
With a file selected, the panel splits into three: the file tree, the code editor, and the debugging pane.

Think of the panel as three columns working together:

1 · File Navigator
index.html
player.js
style.css
logo.png
2 · Code Editor
the selected file’s
source code,
with line numbers
3 · Debugging pane
Watch, Call Stack,
Scope, breakpoints,
step buttons
The Sources panel: files on the left, source in the middle, the debugger controls on the right.
  1. The File Navigator lists every resource attached to the page — HTML, JavaScript, CSS, images. Files injected by Chrome extensions can show up here too.
  2. The Code Editor displays the source of whatever file you selected.
  3. The JavaScript Debugging pane holds the controls you’ll spend the rest of this chapter learning.

Click the same toggle again to hide the file list and give the code more room.

Console

Press Esc and a console slides open at the bottom of the panel. Type any JavaScript expression, press Enter, and it runs immediately in the page’s context — with access to that page’s variables and functions.

The result of each line prints right below it.

12Console1 + 23announce(“Aurora”)undefined
Press Esc and a console strip opens at the bottom — type an expression, press Enter, its result prints below.

For example, 1+2 prints 3. A call like announce("Aurora") runs the function but returns nothing, so the console shows undefined — that’s the function’s return value, not an error.

Breakpoints

Let’s watch the code actually run. In player.js, click directly on the number 4 in the left gutter — on the digit itself, not on the code line. Then do the same on line 8.

1function announce(title) {2 let banner = …34 display(banner);5}67function display(msg) {8 alert(msg);9}
Two breakpoints set by clicking the gutter on lines 4 and 8 — each marked with a tag in the line-number column.

You’ve just set two breakpoints. A breakpoint marks a line where the debugger will automatically pause execution before running that line.

While the script is paused, it’s frozen in place. Nothing else on the page advances. You can read the current value of every variable in scope, run expressions in the console against that live state, and decide where to go next. That frozen moment is the whole point — it’s when debugging happens.

line 1
line 2
line 4 · PAUSED
⏸ inspect here
A breakpoint pauses the program the moment control reaches that line — before it executes.

The right-hand pane keeps a list of all your breakpoints, which pays off once you have many spread across files. From that list you can:

  • Jump straight to a breakpoint’s line by clicking it.
  • Turn one off temporarily by unchecking its box (the line stays marked, but won’t pause).
  • Delete it via right-click → Remove.

The debugger statement

You don’t have to set breakpoints by hand in the UI. You can plant one in the source itself with the debugger keyword:

function announce(title) {
  let banner = `Now playing: ${title}`;

  debugger;  // <-- execution stops here

  display(banner);
}

When execution reaches that line, it pauses exactly as a gutter breakpoint would. The difference is that this pause lives in your code, so it survives across reloads and travels with the file — handy when you want to commit a temporary pause point or share a reproduction.

One caveat: debugger only pauses when developer tools are open. If the tools are closed, the browser skips the statement entirely and the code runs straight through.

Pause and look around

In the example, announce() runs while the page loads. So the simplest way to hit your breakpoints is to reload: press F5 (Windows, Linux) or Cmd+R (Mac).

Execution pauses at line 4, and the debugging pane fills with information about the current moment:

1function announce(title) {2 let banner = …4 display(banner);title = “Aurora”5}Watchbanner.length: 19title === “Aurora”: trueCall Stackannounce(anonymous)ScopeLocaltitle: “Aurora”banner: “Now playing: Aurora”Global
Paused at line 4. The current line is highlighted, and Watch, Call Stack, and Scope now show the live state.

Open the dropdown sections on the right. Each answers a different question about “where am I and what’s true right now?”

Watch
expressions you chose to track — re-evaluated at every pause
Call Stack
the chain of calls that led here — announce() ← (anonymous script)
Scope
variables visible now — Local, then Global, plus this
Three panes describe the paused moment: what you're tracking, how you got here, and what's in scope.
  1. Watch — live values for expressions you pick.

    Click the + and type any expression: a variable name, banner.length, title === "Aurora", whatever. The debugger evaluates it now and re-evaluates it every time execution pauses again, so you can track how a value evolves as you step.

  2. Call Stack — the chain of nested calls.

    Right now the debugger sits inside the announce() call, which was invoked by top-level code in index.html. Since that top-level code isn’t wrapped in a named function, the stack labels it “anonymous.”

    Click any frame in the stack to jump to that function’s paused line and inspect its variables. That’s how you answer “who called this, and with what?”

  3. Scope — the variables you can see from here.

    Local lists the current function’s own variables — Chrome also paints their values inline, right next to the code. Global lists variables declared outside any function. You’ll also spot this, a keyword we cover later; ignore it for now.

Tracing the execution

Once paused, you steer the program forward with the buttons at the top of the debugging pane. Each has a hotkey. The differences between them are subtle but important — they mostly come down to how they treat a function call on the current line.

Resume — F8

Continues running until the next breakpoint, or to the end if there are none. Use it to fly between breakpoints.

7function display(msg) {8 alert(msg);9}Call Stackdisplaynew frameannounce(anonymous)
After Resume, execution stops at the second breakpoint inside display() — the Call Stack has grown by a frame.

After clicking Resume in the example, execution runs on and stops at the breakpoint inside display(). Notice the Call Stack grew by one frame — you’re now one level deeper, inside display().

Step — F9

Runs the next statement, then pauses again. Keep clicking and you march through the script one statement at a time. In the example, the next Step would surface the alert.

Step over — F10

Also runs the next statement, but with one difference: if that statement calls one of your functions, Step over runs the whole call in one go — invisibly — and pauses on the line after it. It doesn’t descend into the function’s body. Reach for this when you trust a function and just want to get past it. (Built-ins like alert never get stepped into anyway.)

Step into — F11

Like Step, but smarter about asynchronous code. If you’re new to JavaScript you can treat it as identical to Step for now — we don’t have async calls yet. The distinction: plain Step walks past scheduled work like setTimeout (whose callback fires later), while Step into will follow into that scheduled code when it eventually runs, waiting if it must. See the DevTools async documentation when you reach that topic.

Step out — Shift+F11

Runs the rest of the current function and pauses right after it returns, back in the caller. This rescues you when you stepped into a function you didn’t actually care about — one click finishes it instead of tediously stepping through every line.

Here’s how the four “step” behaviors differ when the current line calls display():

CommandEnters display()?Where it pauses next
Step (F9)yesfirst line inside display()
Step over (F10)noline after the display() call
Step into (F11)yes (+ async)first line inside display()
Step out (Shift+F11)finishes current fnback in the caller, after this fn returns
Where each command lands when the paused line is a call to your own function display().

Two more controls round out the pane:

  • Enable/disable all breakpoints. A master switch. It doesn’t move execution — it just silences every breakpoint at once, then restores them, without you unchecking each one.
  • Pause on exceptions. When this is on and devtools are open, any error thrown during execution pauses the script at the throwing line. That’s gold for a script that “just dies”: turn it on, reload, and the debugger stops precisely where the error happens, with the full scope and call stack intact so you can see what led to it.

Logging

The debugger isn’t the only window into a running program. Sometimes you just want a running record of what happened, and for that there’s console.log.

Pass it any values and they print to the console:

// open the console to see the output
for (let i = 0; i < 5; i++) {
  console.log("value,", i);
}

That prints value, 0 through value, 4. You can hand console.log multiple arguments — strings, numbers, whole objects — and it lays them out side by side, keeping objects inspectable.

Ordinary visitors never see this output; it lives in the console. To read it, open the Console panel, or press Esc from another panel to pop up the console strip.

Summary

There are three ways to pause a running script:

  1. A breakpoint set in the gutter (optionally conditional).
  2. The debugger statement written into the code.
  3. An error, when devtools are open and Pause on exceptions is enabled.

Once paused, you inspect: read variables in the Scope pane, track expressions in Watch, trace how you arrived via the Call Stack, and move forward with the step commands to find where reality diverges from what you expected.

This chapter covers enough to start debugging real code today. The tools go much deeper — network inspection, performance profiling, memory snapshots — and the full reference lives at https://developer.chrome.com/docs/devtools/.

Honestly, the fastest way to learn devtools is to poke at them. Click around, open the right-click menus, and see what appears. You won’t break anything, and you’ll stumble onto features no tutorial thought to mention.