Debugging in the browser
Pause JavaScript with browser breakpoints, step through function calls, and inspect values with Scope and Watch panels.
- 01Pause code on purposeUse line breakpoints, conditional breakpoints, and the debugger statement to stop the code where you want.
- 02Move through paused codePick Step over, Step into, Step out, or Resume, depending on what you want to find out.
- 03Look at values while pausedRead the Local, Closure, and Global groups in the Scope panel, and pin Watch expressions.
Debugging means pausing your code
A bug is a mistake in a program. Because of it, the program does something you did not expect. Debugging means finding that mistake and fixing it.
The browser has a tool for this, called the debugger. It can pause your JavaScript while it runs. While the code is paused, you can:
- see which line runs next,
- move forward one line at a time,
- look at the values your variables hold right now.
In earlier lessons you used DevTools, the developer tools built into the browser. You used its console and console.log to print clues. Printing is still useful. But the program keeps running while it prints. The debugger is different. It stops the program, so you can look around before anything else changes.
The step-by-step player in this course is a replay: it plays back code that we ran and recorded earlier. It is not a debugger. This lesson uses the replay to explain each idea. Then you try the real debugger in your own browser's DevTools.
Imagine a mystery movie. At full speed, you might miss a clue on a desk. If you pause at the right moment, you can see the clue clearly. The debugger lets you do the same with code: stop at one line, look carefully, then carry on.
- In real life: Pause at the exact scene
- In JavaScript: A breakpoint pauses the code on one exact line
- In real life: Look at the people and things on screen
- In JavaScript: Look at the values in the Scope panel
- In real life: Move forward one frame
- In JavaScript: Step forward to the next line
To find the debugger, open DevTools and pick the right panel:
- In Chrome and Edge, open the Sources panel.
- In Firefox, open the Debugger panel.
- In Safari, first turn on the developer features in Safari's settings. Then open the Sources panel.
The names are different in each browser. The ideas in this lesson are the same in all of them.
Breakpoints: tell the browser where to pause
BREAKPOINTSA breakpoint is a marker you put on a line of code. When JavaScript reaches that line, the browser pauses before it runs the line. To set one in most browsers, open your file in the debugger panel and click the line number.
const cart = { total: 20 };cart.total = cart.total + 5;console.log(cart.total);Line 1 makes a cart object with a total of 20. Line 2 adds 5 to the total. Line 3 prints the total.
Put a breakpoint on line 2. The browser pauses before line 2 runs. The Scope panel lists your variables and the values they hold right now. It shows cart.total as 20. Now press Step over, the button that runs one line. Line 2 runs, and cart.total is now 25. Then line 3 prints 25.
cart.total is still 20. Press Step over: line 2 runs, and the same value is now 25.This is the main question a debugger answers: what value did a line get, and what did it change?
The debugger has several kinds of breakpoints:
- Line breakpoint: pauses every time one line runs.
- Conditional breakpoint: pauses only when a test you write is
true. For example,i === 50inside a loop. - Logpoint: prints a message when a line runs. It does not pause, and it does not change your file.
- Pause on exceptions: pauses on the line where an error is thrown. An exception is another word for a thrown error. Many browsers can also pause on errors that your code catches with
try/catch. - DOM and event listener breakpoints: pause when part of the page changes, or when an event such as a click happens. The DOM is the browser's model of the page. These are browser features. They are not new JavaScript code.
// Line breakpoint: pause whenever this line runs.total = total + price; // Conditional breakpoint: pause only when this is true.i === 50 // Logpoint: print a message without pausing."total is", total // Pause on exceptions: stop where an error is thrown.throw new TypeError("price must be a number");This block is not one program to run. Each comment names one kind of breakpoint, and the line under it shows where it goes. In plain words, line by line:
- Line 2 is a line to pause on, with a line breakpoint.
- Line 5 is the test you type for a conditional breakpoint.
- Line 8 is the message you type for a logpoint.
- Line 11 throws an error. Pause on exceptions would stop on this line.
The table below compares four of these tools. Read each row as: "I use this tool when…, and then this happens."
| Tool | When does it help? | What happens? |
|---|---|---|
| Line breakpoint | You know which line looks wrong. | The code pauses every time that line runs. |
| Conditional breakpoint | A loop or a function goes wrong in only one case. | The code pauses only when your test is true. |
| Logpoint | You want to print a clue without changing your file. | DevTools prints a message, and the code keeps running. |
| Pause on exceptions | An error happens, but you don't know where. | The code pauses on the line that throws the error. |
Conditional breakpoints help a lot in loops. Say a list has 100 items, and only item 50 is wrong. A normal breakpoint would pause 50 times before you reach it. A test such as i === 50 lets the browser skip straight to the item you care about.
const score = 42;console.log(score === 42);Line 1 stores 42 in score. Line 2 prints the result of score === 42, which is true. So a conditional breakpoint with the test score === 42 would pause here. If you change the score to 41, the test gives false, and the browser keeps running. The test only reads values. It does not change them.
The debugger statement
REAL DEVTOOLSJavaScript has a built-in statement called debugger. A statement is one instruction in your code. When DevTools is open, the browser pauses on the debugger; line, just like at a breakpoint. When DevTools is closed, browsers usually ignore it and keep running.
It helps in two cases:
- You can't easily find your file in the debugger panel. Tools that pack your code into one big file can make this hard.
- You want a quick pause in a small test on your own computer.
It is also easy to forget. Remove debugger; before you share your code.
function saveTodo(todo) { debugger; console.log(todo + " saved");} saveTodo("tea");Line 2 pauses the code, but only for someone who has DevTools open. Press Resume, the button that lets the code keep running. Then line 3 prints tea saved. This is handy for a quick check. In code you share, a line breakpoint is usually better, because it leaves nothing behind in the file.
function handleDebugClick() { const label = "Debug button"; debugger; console.log(label + " finished");} document.querySelector("button").addEventListener("click", handleDebugClick);Open DevTools first. Then click the button inside the small page in the frame. Its code is fixed and safe. The line to watch is debugger;.
debugger; before you share your code.The button sits in a small separate page inside this one, called an iframe. Its code is fixed and safe. If your DevTools was open, the browser paused on the real debugger; line. If DevTools was closed, nothing special happened, and the code kept running.
Step over, step into, and step out
STEP THROUGHWhen the code is paused, the debugger's toolbar has buttons that move it forward. In Chrome and Edge, they are:
- Resume (
F8): keep running until the next breakpoint. - Step over (
F10): run the current line, then pause on the next one. - Step into (
F11): if the line calls a function, go inside that function and pause on its first line. - Step out (
Shift+F11): finish the function you are in, then pause on the line that called it.
Other browsers have the same buttons, but the keys can be different.
totalCart(items); // Step into opens totalCartapplyDiscount(total); // Step over runs it and stays herereturn roundPrice(value); // Step out finishes this functionThis block is not one program to run. Each line shows one call, and its comment names a button. In plain words:
- Step into goes inside
totalCart, so you can read its lines. - Step over runs
applyDiscountand pauses on the next line, without going inside. - Step out finishes the function you are in, and goes back to the line that called it.
function addTax(price) { return price + 2;} const total = addTax(5);console.log(total);Lines 1 to 3 make a function, addTax, that adds 2 to a price. Line 5 calls addTax(5) and stores the result in total. Line 6 prints it.
Pause on line 5. Then:
- Step over runs
addTax(5)without showing its lines. It stores 7 intotaland pauses on line 6. - Step into goes inside
addTaxfirst, and pauses on line 2.
Both ways print the same result: 7. The button changes only what you see. It does not change what the code does.
addTax(5), and both give 7. Step over keeps the function closed and stays on line 5. Step into opens it, so you can read its line 2.If a chapter looks fine, you read its title and move on. If it might hold the clue, you open it and read it. If you are inside a chapter and the clue is not there, you finish it and go back to the main story.
- In real life: Read the chapter title and move on
- In JavaScript: Step over: run the function call without going inside
- In real life: Open the chapter and read each paragraph
- In JavaScript: Step into: go inside the function that is called
- In real life: Finish the chapter and go back to the main story
- In JavaScript: Step out: finish the current function and go back to the line that called it
The next replay runs one program that has a bug. A cart holds two prices, 20 and 10, so the total is 30. The program should take 10 percent off once. That gives 27. But the bug takes 10 percent off twice. Pick Step into or Step over. Both show the same wrong result: 24.3.
Step into goes inside each function call. Watch the second applyDiscount call show the bug: the discount is taken off twice.
script
function roundPrice(amount) { return Math.round(amount * 100) / 100;} function applyDiscount(amount) { const discounted = amount * 0.9; return roundPrice(discounted);} function totalCart(prices) { let total = 0; for (const price of prices) { total = total + price; } const firstPass = applyDiscount(total); const finalTotal = applyDiscount(firstPass); return finalTotal;} console.log(totalCart(items));The bug is on line 18: applyDiscount(firstPass). Step into shows it clearly. Inside the second call to applyDiscount, you can see that amount is already 27. With Step over, the function still runs, but you see only the value it gives back, not its lines.
Step out helps when you are already inside a function. For example, if you are inside roundPrice, Step out finishes roundPrice and takes you back to applyDiscount, the function that called it.
The Scope panel and Watch expressions
SIMULATORWhen the code is paused, the debugger shows what each name holds at that moment. The Scope panel lists the variables your code can reach right now. It puts them in groups:
- Local: variables inside the function that is running now.
- Closure: variables from an outer function that the current function still remembers. The Closures lesson explains this later.
- Global or Script: variables made at the top level of the page or script, outside any function.
One detail: a top-level let does not become a property of window, the object that stands for the browser window. DevTools may still list it under Script.
An expression is a piece of code that gives a value, like 2 + 3. A Watch expression is an expression you pin to the debugger, like a gauge on a car's dashboard. Each time the code pauses, the browser works out its value again from the current values. Good watches are small, like subtotal * discountRate, items.length, or total === 27.
const subtotal = 30;const discountRate = 0.1;console.log(subtotal * discountRate);Line 1 stores a subtotal of 30. Line 2 stores a discount rate of 0.1, which means 10 percent. Line 3 prints subtotal * discountRate, which is 3.
Pause after line 2 and pin subtotal * discountRate in Watch. It shows 3, the same as the console. This watch only reads values, so it is safe to work out again and again. A watch such as cart.add() is different. It would add to the cart every time DevTools works it out.
subtotal * discountRate only reads, so it always shows 3. cart.add() changes the cart, so each pause adds one more item.Picture a desk with drawers. The drawer in front of you is the easiest to reach. Another drawer came with you from home. A big cabinet in the corner is shared by everyone. When the code pauses, the Scope panel opens every drawer the current function can reach, and shows you what is inside.
- In real life: The drawer right in front of you
- In JavaScript: Local: names in the current function
- In real life: A drawer the function brought with it from home
- In JavaScript: Closure: names remembered from an outer function
- In real life: The big filing cabinet everyone shares
- In JavaScript: Global/Script: top-level names
Start with one small calculation. In the replay, pause inside calculateTotal to see its Local values. When the function finishes, its Local values are gone. The top-level total stays.
Follow this calculation. See which values are Local during the call, and which stay at the top level of the script afterward.
script
function calculateTotal(amount, discountRate) { const discount = amount * discountRate; return amount - discount;} const discountRate = 0.1;const total = calculateTotal(subtotal, discountRate);console.log(total);Line 2 makes the Local value discount. A Watch for amount * discountRate would show 3 at that pause. Line 8 stores the 27 that the function gives back in total, at the top level of the script. That is why the next pause no longer shows the function's Local values.
calculateTotal runs, Scope shows its Local values, like discount. After it finishes, those values are gone. Only the top-level total is left.const subtotal = 30;const discountRate = 0.1; function applyDiscount(amount) { const discounted = amount - amount * discountRate; return Math.round(discounted * 100) / 100;} const total = applyDiscount(subtotal);Scope · Local
3027Scope · Global/Script
300.1Watch
327The Local values belong to applyDiscount. The Global values were made outside the function. Watch works out each expression again from the paused values.
DevTools works out every Watch expression again at each pause. So pick watches that only read values. Avoid watches that change the page or your data. A watch such as cart.add() would add to the cart each time it runs.
Which tool fits?
INTERACTIVEDebugging is faster when you pick the smallest tool that answers your question. First, here is what to do each time the code pauses.
- Read the highlighted line before you change anything.
- Check Scope. Compare the value you expected with the value you got.
- Read the call stack if you need to know which function called this one. The call stack is the list of functions that are running, newest first.
- Press Resume when this pause has answered your question. The code runs on to the next breakpoint.
Now sort each case below by the kind of tool you would use first.
- Pause every time one line that looks wrong runs
- A loop goes wrong only when
i === 50 - Print
totalfrom a file you don't want to change - Print a quick note while you write a tiny test on your own computer
- See
subtotal * discountRateat every pause - See the current function's
price,total, and other nearby variables - The code throws an error, and you don't know which line caused it
- Some other code catches the error, so the console never shows where it started
Put each case in the group of tools that fits it best. Each explanation names the exact tool: a line breakpoint, a conditional breakpoint, a logpoint, Watch, console.log, or Pause on exceptions.
Where you will use this
The debugger helps most when the code looks right, but the values are wrong. A shopping cart, a form check, a quiz score, or a button can all go wrong because one value changed earlier than you thought.
const form = { email: "" };const canSubmit = form.email !== "";console.log(canSubmit);Line 1 makes a form with an empty email. Line 2 sets canSubmit to true only if the email is not empty. Line 3 prints the result.
Say the form's Submit button won't work. Put a breakpoint on line 2 instead of guessing. Scope shows form.email as an empty string, "". So canSubmit becomes false, and line 3 prints false.
Here is the full cart program from the replay again. Lines 17 and 18 are highlighted. Try the routine below on it.
const items = [20, 10]; function roundPrice(amount) { return Math.round(amount * 100) / 100;} function applyDiscount(amount) { const discounted = amount * 0.9; return roundPrice(discounted);} function totalCart(prices) { let total = 0; for (const price of prices) { total = total + price; } const firstPass = applyDiscount(total); const finalTotal = applyDiscount(firstPass); return finalTotal;} console.log(totalCart(items));A good debugging routine has five steps:
- Say what goes wrong, in one sentence.
- Set a breakpoint just before the value first looks wrong.
- Step over functions you trust. Step into functions that might hide the bug.
- Read Scope before you guess. Add a Watch for any value you keep working out by hand.
- Fix the smallest line you can. Then run the program again from the start.
Common mix-ups
“Step over means skip this function.”
Step over still runs the function. It only stops you from seeing the function's lines in the debugger.
“The Scope panel shows what variables will be later.”
Scope shows values right now: on the paused line, at that moment.
“A watch expression is just text.”
DevTools runs it as code. So keep watches safe: they should only read values, never change them.
“The course replay is DevTools.”
The replay plays back real functions, so you can learn the steps. The real debugger is in your browser's DevTools.
“debugger; is harmless to leave in code.”
It is only for a short time. It pauses anyone who opens DevTools, and it makes shared code look unfinished.
Each row of the table below compares two tools that are easy to mix up. Read it as: "Use the first tool when…, and the second tool when…"
| Two tools | Use the first one when | Use the second one when |
|---|---|---|
Breakpoint or console.log | You need to pause and look at several values. | You only need one quick printed clue. |
| Step over or Step into | You trust the function and only want its result. | You need to read the function line by line. |
| Scope or Watch | You want to see all the nearby variables. | You want one pinned expression at every pause. |
Practice: pause, inspect, decide
5 EXERCISESTry each exercise before you open the hints. Some ask what a value would be at a pause. Others ask you to use the real debugger in your own browser.
Imagine a breakpoint on return result;. What value would the Scope panel show for result?
function double(number) {
const result = number * 2;
return result;
}
console.log(double(6));Paused on return result;, the Local group in Scope shows number as 6 and result as 12. Later the program prints 12.
A loop goes wrong only on item 50. What test would you type so the browser pauses only when i is 50?
i === 50Right-click the line number (or Control-click on a Mac). Choose to add a conditional breakpoint, and type i === 50. The loop runs without pausing until that test is true.
Add a short pause with debugger;. Try it with DevTools open, then remove it. Which statement must you not leave behind?
function checkForm() {
debugger;
return true;
}Open DevTools and run the code. The browser pauses on debugger;. When you are done looking, delete that line, so the code doesn't pause for other people.
Say the code pauses after line 3. What would a Watch expression for subtotal * discountRate show?
const subtotal = 30;
const discountRate = 0.1;
const total = subtotal - subtotal * discountRate;
console.log(total);subtotal * discountRate is 30 * 0.1, so Watch shows 3. The final total is 27, because line 3 takes that 3 away from the subtotal.
In the cart program, which line takes the discount off for the second time?
const firstPass = applyDiscount(total);
return firstPass;Line 17 takes the discount off once, which is correct. Line 18 calls applyDiscount(firstPass), so 27 becomes 24.3. Remove the second discount and return firstPass.
Quiz: check your understanding
7 QUESTIONSPick an answer and read why it is right or wrong. Then think about which DevTools button or panel you would use.
Question 1 of 7What is a breakpoint?
Choose an answer to see the explanation.
Question 2 of 7When does a
debugger;statement pause?Choose an answer to see the explanation.
Question 3 of 7A line calls
applyDiscount(total). You trust that function, and you want to stay in the current one. Which button fits?Choose an answer to see the explanation.
Question 4 of 7What does the double-discount program print?
Read the code, then predictfunction discount(amount) { return amount * 0.9; } const first = discount(30); const second = discount(first); console.log(second);Choose an answer to see the explanation.
Question 5 of 7Which Scope group usually shows the variables made inside the function that is paused?
Choose an answer to see the explanation.
Question 6 of 7Which tool prints a message at a line, without pausing and without changing your file?
Choose an answer to see the explanation.
Question 7 of 7What does the loop print?
Read the code, then predictlet total = 0; for (let i = 1; i <= 3; i = i + 1) { total = total + i; } console.log(total);Choose an answer to see the explanation.
Key takeaways
- A breakpoint pauses the code, so you can see the exact moment a value changes.
debugger;pauses the code when DevTools is open. Remove it when you are done.- Step over runs a function call without going inside. Step into goes inside. Step out finishes the current function.
- Scope shows the values your code can reach right now. Watch shows the expressions you pinned, worked out again at every pause.
- The course replay helps you learn. Your browser's DevTools debugs real code.
Remember the one-liner.
Pause the code, move one line at a time, and check the values before you guess.
Up next: Coding style & naming.