JAVASCRIPT / ERRORS, STRICT MODE, AND DEBUGGING
Debugging with breakpoints and the debugger word
Pause code with DevTools breakpoints or a debugger statement, inspect live variables and the call stack, and step through execution frame by frame.
What you will learn
- Set line and conditional breakpoints from the Sources panel without editing the file
- Guard a debugger statement with an if so it fires only on the run that matters
- Read locals, closure variables, and this from the Scope pane while execution is paused
- Pick step over, step into, or step out by whether you care about the callee's frame
Understanding Debugging with breakpoints and the debugger word
A breakpoint is a request to the engine: suspend before this statement runs and hand control to the inspector. Because JavaScript runs on one thread, suspending it freezes everything the page does, while the call stack and every live binding stay exactly as they were. That is the real advantage over printing values: instead of only what you thought to serialize when you wrote the log line, you get the whole frozen machine and can ask it new questions. The debugger statement is that same request written in source code, standardized so an engine breaks on the line when an inspector is attached and skips it when none is.
Once paused, the Console evaluates inside the selected frame's scope, so you can call the suspect function again with different arguments or read a closure variable that never appears in any output. The stepping controls move a cursor over stack frames, not over lines of text: step over runs a call to completion and stops on the next statement of the current frame, step into pushes the callee's frame and stops on its first line, step out finishes the current frame and stops back in the caller. Choosing between them is really the question of whether you trust the function on that line, which is why stepping into everything strands you inside library code.
Use a DevTools breakpoint for anything you can reach by clicking a line number, and reach for a debugger statement when the code runs before you can click: module top level, an early event listener, a worker, an injected script. Both belong in your working copy only, since a debugger statement that reaches production freezes every visitor who has DevTools open, so let the no-debugger lint rule or the bundler's drop_debugger option catch it. Remember also that a pause burns real wall-clock time inside your program, so timing-sensitive behaviour can vanish, appear, or turn into a timeout while you sit on the line.
A breakpoint suspends the single JavaScript thread just before a chosen statement runs so you can read live bindings and walk the call stack, and the debugger statement is that request expressed in the source.
function priceWithTax(price, ratePercent) {
const rate = ratePercent / 100;
debugger; // execution stops here, before the next line runs
const total = price + price * rate;
return Math.round(total * 100) / 100;
}
console.log(priceWithTax(19.99, 8.25));
console.log('the debugger statement did nothing: no inspector is attached');A breakpoint suspends the one JavaScript thread immediately before a statement executes, freezing the call stack and every live binding so you can inspect and step through them.
Worked examples
Pause only on the row that is wrong
Guarding a debugger statement with a condition turns a pause you would hit on every iteration into one you hit once.
const rows = [
{ id: 1, qty: 2 },
{ id: 2, qty: '3' },
{ id: 3, qty: 4 },
];
let units = 0;
for (const row of rows) {
if (typeof row.qty !== 'number') {
debugger; // fires on the single bad row, not on all three
console.log('suspect row:', row.id, typeof row.qty);
}
units += row.qty;
}
console.log('units:', units, typeof units);Example explained
Line 1The typeof test is the condition, so the two healthy rows run at full speed and only row 2 stops you.
Line 2At that pause point units is still 2, because the breakpoint fires before the addition on the next line.
Line 3units += row.qty concatenates once either operand is a string, which is how a numeric total ends up as '234'.
Line 4A hand-written guard like this is the source-code version of a conditional breakpoint set on the line in DevTools.
What is not readable at the pause point
A breakpoint placed above a const declaration cannot read that variable, because the binding is still in its temporal dead zone.
function average(values) {
const count = values.length;
try {
// a breakpoint on this line, then typing mean in the console, does this
console.log(mean);
} catch (err) {
console.log('not readable yet:', err.name);
}
const mean = values.reduce((sum, n) => sum + n, 0) / count;
return mean;
}
console.log(average([2, 4, 6]));Example explained
Line 1The pause point sits above const mean, so the binding exists in the scope but has no value yet.
Line 2Reading it throws a ReferenceError instead of giving undefined, so a breakpoint one line too high makes a perfectly good variable look missing.
Line 3count was initialized on the previous line, so at the same pause point it reads back as 3.
Line 4Move the breakpoint below the declaration, or step over once, when you need to see mean.
A pause costs real time
Suspending execution stops your code but not the clock or the network, so anything that measures elapsed time reads differently across a breakpoint.
const start = Date.now();
debugger; // with DevTools open, wait a few seconds before resuming
const held = Date.now() - start > 1000;
console.log('did the breakpoint leak into my own timing?', held);Example explained
Line 1With no inspector attached the statement is skipped, both Date.now() calls happen in the same millisecond, and held is false.
Line 2Pause on that line with DevTools open, wait, then resume, and the identical code prints true.
Line 3Retry backoff, slow-request checks, animation timing and setTimeout deadlines all read garbage across a pause.
Line 4The event loop is stalled while you are paused, so timers and network callbacks queue up and fire in a burst the moment you resume.
Important notes
The pause happens before the highlighted line executes, so a let or const on that line or below is still uninitialized and reading it in the Console throws a ReferenceError.
Breakpoints you click in DevTools are stored by the browser per URL, not in your file, so they survive reloads but do not travel to teammates; with a bundler, source maps decide whether a breakpoint in original source binds to a real line in the shipped code.
Common mistakes
Committing a debugger statement. The next visitor who has DevTools open freezes on that line and reports the app as broken, and nothing about it shows up in your error logs.
Dropping a bare debugger inside a loop over a large array, then holding the resume key. You hit the same line thousands of times and give up before reaching the element that matters; guard it with an if or use a conditional breakpoint.
Running node app.js and expecting a debugger statement to stop. With no inspector attached the statement has no effect, so the script runs to the end; use node --inspect-brk app.js or node inspect app.js.
Try it yourself
Change, predict, then run
Write a function that averages an array where one element is the string '10', then set a conditional breakpoint or a guarded debugger statement that fires only when typeof value is not 'number'. Use the Scope pane to report the offending element and the running total at that instant.
Open the JavaScript workspaceCheck your understanding
A race condition shows up roughly once in ten page loads, but never when you pause on a breakpoint just before the fetch callback. What best explains that?
- Breakpoints suspend pending network requests until you press resume, so the responses can no longer overlap.
- The debugger re-executes the paused function from its first line on resume, which hides the ordering bug.
- Only the JavaScript thread is suspended: responses still arrive, their callbacks wait in the queue and then run back to back after resume, so the interleaving changes.
- Debugger statements and breakpoints are ignored inside async functions, so the callback never really paused.
Show answer
Pausing stops your code, not the browser's network stack or the clock, so work that normally interleaves with your running function is serialized into a burst after resume and the racy ordering disappears. Option 0 is tempting because nothing appears to move on screen, but requests keep completing while you are paused, which is also why elapsed-time checks read absurd values across a breakpoint.