Debugging
Learn how to systematically find and fix bugs in JavaScript using the console API, breakpoints, watch expressions, the call stack, and the key panels of browser DevTools.
Debugging is a skill on its own, separate from writing code in the first place — it's the process of forming a hypothesis about why something is wrong and using the right tool to confirm or rule it out quickly, rather than guessing and re-reading code line by line.
The console object
console is a built-in global object with several methods for writing information to the browser's DevTools console (or a terminal, in Node.js), each suited to a different purpose. console.log() is the general-purpose workhorse for printing values; using it to inspect a variable's value at a specific point in your code — sometimes called 'console.log debugging' — is often the fastest way to answer a quick question about what's happening.
console.table() is especially useful for arrays of objects: it renders them as an actual formatted table, with one column per property, which is far easier to scan than a wall of nested object logs. console.error() prints in a visually distinct (often red) style and, importantly, includes a stack trace showing where it was called from — use it for genuine problems, not routine output, so real errors stand out from ordinary logs.
• console.log(value) — general-purpose output • console.table(arrayOfObjects) — tabular view of structured data • console.warn(msg) — a cautionary message, styled distinctly • console.error(msg) — an error message with a stack trace • console.group(label) / console.groupEnd() — visually nest related logs together
Breakpoints
A breakpoint pauses JavaScript execution at a specific line, letting you inspect the exact state of the program at that moment — every variable's current value, not just the ones you thought to log. In DevTools' Sources panel, clicking a line number in your script sets a breakpoint there; execution halts right before that line runs the next time it's reached.
The debugger; statement does the same thing directly from your code — if DevTools is open, execution pauses there automatically, which is convenient for a condition that's hard to reach by clicking around in the Sources panel first.
Watch expressions and the call stack
While paused at a breakpoint, the Watch panel lets you add any expression (a variable name, or something more complex like items.length or user.address.city) and see its live value update as you step through code line by line — useful for tracking one specific value across multiple steps without re-checking it manually each time.
The Call Stack panel shows the chain of function calls that led to the current paused line — which function called which, all the way back to where execution started. This is essential for understanding not just what the current state is, but how the program actually got here, especially several layers deep into nested function calls.
Stepping through code
Once paused at a breakpoint, DevTools gives you fine control over execution: Step Over runs the current line and pauses at the next one, without entering any function it calls. Step Into does enter a function call on the current line, letting you follow execution inside it. Step Out finishes the current function immediately and pauses back in whoever called it, and Resume simply continues normal execution until the next breakpoint is hit (or the script finishes).
Source maps
Real-world JavaScript is often minified or compiled from TypeScript, JSX, or a bundler before it reaches the browser, so the code actually running looks nothing like the code you wrote. A source map is a file that tells DevTools how to translate positions in that transformed code back to positions in your original source, so breakpoints, error stack traces, and the Sources panel can show you your actual, readable source files instead of a single unreadable minified line.
The Network, Elements, Performance, and Application tabs
The Network tab lists every request the page makes (scripts, images, API calls), showing status codes, timing, headers, and response bodies — the first place to look when data isn't loading or an API call is failing. The Elements tab shows the live DOM tree and computed CSS for the page as it currently exists (which can differ from the original HTML source after JavaScript has modified it), and lets you edit either directly to test a fix live.
The Performance tab (introduced in the performance lesson) records a timeline of scripting, rendering, and painting work to find slow frames and long-running tasks. The Application (sometimes labeled Storage) tab lets you inspect and edit what's stored on the client: localStorage, sessionStorage, cookies, and IndexedDB — invaluable when a bug depends on stale or unexpected stored data.
Common console and debugger patterns
console.group()/groupEnd() visually indents a related block of logs, making dense output easier to scan. A debugger; statement is only meaningful while DevTools is open — with DevTools closed it is simply skipped, so it's safe to commit temporarily but should still be removed before shipping.
console.log("user:", user);
console.table(users);
console.error("Failed to load profile", error);
console.group("Checkout process");
console.log("cart total:", total);
console.log("applied coupon:", coupon);
console.groupEnd();
function calculateDiscount(price, percent) {
debugger; // execution pauses here when DevTools is open
return price - price * (percent / 100);
}Code Examples
console.table() renders an array of similarly shaped objects as an actual table with one row per object and one column per property, which is much faster to scan than the default nested object output from console.log(). Expected result: A formatted table with columns "(index)", "name", "role", and rows for Ana/admin, Ben/editor, Cy/viewer.
console.table(users) automatically detects the common properties (name, role) and turns them into columns.
Placing a debugger; statement inside the function pauses execution the moment this function runs (with DevTools open), letting you inspect price, coupon, discount, and finalPrice one at a time as you step through, rather than guessing which one is wrong from the final output alone. With DevTools open, execution would pause on the debugger; line first; using Step Over would then run the discount calculation line and let you inspect the resulting discount value before moving on.
debugger; is equivalent to manually clicking a line number to set a breakpoint, but lives directly in the source and travels with the code.
If you set a breakpoint inside applyTax, the Call Stack panel would show applyTax was called from calculateTotal's reduce callback, which was called from processOrder, which was called from the top-level script — letting you trace exactly which order and which item triggered the wrong value, instead of only seeing the final incorrect total.
Reading the Call Stack panel top to bottom shows the exact chain: applyTax <- calculateTotal <- processOrder <- (global).
Common mistake: applyTax computes taxRate but never actually uses it in the returned value — a bug that a plain console.log of the final total wouldn't clearly point to, but that stepping through with a breakpoint immediately reveals.
const users = [
{ name: "Ana", role: "admin" },
{ name: "Ben", role: "editor" },
{ name: "Cy", role: "viewer" }
];
console.log(users); // one nested, harder-to-scan log line
console.table(users); // a real formatted table with name/role columnsfunction getDiscountedPrice(price, coupon) {
debugger; // pauses here whenever DevTools is open and this runs
const discount = coupon ? coupon.percentOff : 0;
const finalPrice = price - price * (discount / 100);
return finalPrice;
}
getDiscountedPrice(80, { percentOff: 25 });function processOrder(order) {
return calculateTotal(order.items);
}
function calculateTotal(items) {
return items.reduce((sum, item) => sum + applyTax(item.price), 0);
}
function applyTax(price) {
// Bug: forgot to actually add the tax
const taxRate = 0.08;
return price; // should be: price + price * taxRate
}
processOrder({ items: [{ price: 100 }, { price: 50 }] });The call stack while paused inside applyTax
Each frame represents one function call still in progress; reading from top to bottom traces exactly how execution arrived at the currently paused line.
Call Stack (top = current frame)
┌─────────────────────────────┐
│ applyTax(price) │ <- currently paused here
├─────────────────────────────┤
│ (anonymous) — reduce callback│
├─────────────────────────────┤
│ calculateTotal(items) │
├─────────────────────────────┤
│ processOrder(order) │
├─────────────────────────────┤
│ (top-level script) │
└─────────────────────────────┘Important Rules
Use console.error() (not console.log()) for genuine problems — it includes a stack trace and stands out visually from routine output.
console.table() is almost always clearer than console.log() for arrays of similarly-shaped objects.
A breakpoint (or a debugger; statement) lets you inspect the FULL state of the program at a moment in time, which is strictly more powerful than a handful of scattered console.log() calls.
The Call Stack panel tells you HOW execution reached the current line, not just what the current values are — read it when a value is wrong but you don't know which caller is responsible.
Remove debugger; statements and stray console.log() calls before shipping code to production.
Where this shows up
Every professional JavaScript developer debugs this way daily: checking the Network tab when an API call silently fails, setting a breakpoint to catch exactly which item in a loop has bad data, reading a Call Stack to find which of several callers passed a bad argument, and relying on source maps so that a minified production error still points back to a specific line in the original source file, not an unreadable one-line bundle.
// Sprinkling console.log(1), console.log(2), console.log(3) throughout a function to guess where it fails
// Set a single breakpoint at the suspicious line and step through with real variable values visible
Numbered console.log() markers only tell you which line was reached, not the actual values involved at that moment — a breakpoint gives you the full live state and is faster to work with for anything beyond a trivial bug.
console.log("Failed to fetch user data"); // looks like every other log line console.error("Failed to fetch user data", error);
Using console.log() for real errors makes them blend in with routine output and lose their stack trace; console.error() is styled distinctly and captures exactly where the error occurred.
[Multiple Choice] Which DevTools panel would you check first to find out why an array of similarly-shaped user objects is hard to read as plain nested console.log() output? A. Use console.table() instead B. Use console.warn() instead C. Use console.group() instead D. Use console.count() instead
A. Use console.table() instead — console.table() renders an array of similarly-shaped objects as an actual formatted table with one row per object and one column per shared property, which is far easier to scan than nested console.log() output.
[True / False] The Call Stack panel, while paused at a breakpoint, shows how execution reached the current line — which function called which, in order. A. True B. False
A. True — True. The Call Stack lists every function call still in progress, from the currently paused frame back to where execution originally started, showing the exact chain of calls that led there.
A function sumPositive(numbers) is supposed to return the sum of only the positive numbers in the array, but it's returning the wrong result for [3, -2, 5, -1, 4]. Describe the debugging steps you'd take using breakpoints, watch expressions, and stepping, to find the bug (you do not need to know the exact code — describe your process).
1) Set a breakpoint on the first line inside sumPositive (or add a debugger; statement there). 2) Add the accumulator/sum variable and the current numbers array to the Watch panel so their values update live. 3) Step Over each iteration of the loop/reduce, watching the sum after each step to see exactly which number causes an unexpected change (e.g. a negative number being added when it should have been filtered out). 4) Once the wrong step is found, inspect the condition guarding the addition (e.g. an inverted or missing comparison like num > 0) to confirm the exact cause.
Think about where you'd place the breakpoint, what you'd add to Watch, and whether you'd Step Over or Step Into as the loop/reduce runs.