JavaScript Debugging and Browser DevTools: A Practical Guide
Learn to debug JavaScript with console methods, breakpoints, the debugger statement, call stack, watch, Network and DOM inspection in browser DevTools.
What you will learn
Every developer, from beginner to expert, spends a large part of the day fixing bugs. The difference is that experienced developers have a method and they know their tools. In this lesson you will learn how to read error messages, how to use the console properly (not just console.log), how to pause your code with breakpoints and the debugger statement, how to walk through code step by step, how to use the Call Stack, Scope and Watch panels, and how to use the Network, Elements and Application panels to find problems in real websites.
What is debugging?
A bug is a mistake in code that makes it behave in the wrong way. Debugging is the process of finding the cause of the bug and fixing it. Your main tool is the browser DevTools, which are built into Chrome, Edge, Firefox, Safari and other browsers.
Opening DevTools
| Action | Windows / Linux | Mac |
|---|---|---|
| Open DevTools | F12 or Ctrl + Shift + I |
Cmd + Option + I |
| Open the Console directly | Ctrl + Shift + J |
Cmd + Option + J |
| Inspect an element | Right click, then Inspect | Right click, then Inspect |
| Command menu (search any tool) | Ctrl + Shift + P |
Cmd + Shift + P |
(In Firefox the Console shortcut is Ctrl + Shift + K.)
The panels you will use most (names are from Chrome, other browsers are similar):
| Panel | Use it to |
|---|---|
| Elements | Inspect and change the HTML and CSS live |
| Console | See messages and errors, and run JavaScript |
| Sources (Debugger in Firefox) | Read your code, set breakpoints, step through it |
| Network | See every request: files, API calls, errors, speed |
| Application (Storage in Firefox) | Look at localStorage, sessionStorage, cookies |
| Performance and Lighthouse | Find what makes the page slow |
Step 1: Read the error message
Most bugs announce themselves in the Console as a red error. Learn to read it:
Uncaught TypeError: Cannot read properties of undefined (reading 'name')
at showUser (app.js:12:18)
at init (app.js:25:3)
| Part | Meaning |
|---|---|
Uncaught |
Nobody handled this error with try...catch |
TypeError |
The kind of error (you learned the types in the error handling lesson) |
Cannot read properties of undefined (reading 'name') |
You wrote something.name, but something is undefined |
at showUser (app.js:12:18) |
The error happened in function showUser, file app.js, line 12, column 18 |
at init (app.js:25:3) |
showUser was called from init at line 25 (this list is the stack trace) |
Click the file name in the Console and DevTools takes you straight to that line. Start looking there, and then look at the functions below it in the stack trace.
The most common error messages
| Message | Usual cause |
|---|---|
x is not defined |
Typo in a name, or the variable is out of scope |
Cannot read properties of undefined (reading 'y') |
The thing before .y is undefined (wrong variable, data not loaded yet, wrong index) |
x is not a function |
You called something that is not a function (wrong name, or an object instead of a method) |
Unexpected token |
A syntax mistake: missing bracket, comma or quote |
Assignment to constant variable |
You changed a const |
Failed to fetch / CORS error |
Network problem or the server does not allow your site |
Step 2: Use the Console well
console.log is great, but the Console has many more tools.
Different kinds of messages
console.log("Normal message");
console.info("Information");
console.warn("Warning: something looks wrong");
console.error("Error: something failed");
Warnings are yellow and errors are red, so they are easy to find in a long output. Use the filter buttons in the Console to show only one kind.
Log several values with labels
Do not log a bare variable. Add a label so you know what you are looking at:
const user = { name: "Riya" };
const age = 22;
console.log("user:", user, "age:", age);
console.log({ user, age }); // trick: an object with names as keys
The second line prints { user: {...}, age: 22 }, labelled automatically.
Format placeholders
console.log("Hello %s, you are %d years old", "Riya", 22);
// %s = string, %d = number, %o = object
console.log("%cStyled text", "color: white; background: purple; padding: 4px;");
// %c applies CSS to the text after it
console.table: arrays and objects as a table
const products = [
{ name: "Pen", price: 10, stock: 100 },
{ name: "Book", price: 120, stock: 15 },
{ name: "Bag", price: 800, stock: 0 }
];
console.table(products);
You get a neat table with a column for each key. Click a column header to sort it.
console.group: organise long output
console.group("Order #101");
console.log("Item: Pen");
console.log("Qty: 2");
console.group("Payment");
console.log("Method: UPI");
console.groupEnd();
console.groupEnd();
Use console.groupCollapsed() to start the group closed.
console.time: measure speed
console.time("sum loop");
let sum = 0;
for (let i = 0; i < 1000000; i++) sum += i;
console.timeEnd("sum loop"); // sum loop: 3.2 ms (your time will differ)
The label must be the same in both calls.
console.count: how many times did this run?
function handleClick() {
console.count("handleClick");
}
handleClick(); // handleClick: 1
handleClick(); // handleClick: 2
console.assert: log only when something is wrong
const total = 130;
console.assert(total === 120, "Total is wrong!", total);
// Prints an error only when the condition is false
console.trace: who called this function?
function inner() {
console.trace("Where am I called from?");
}
function outer() {
inner();
}
outer(); // prints the stack trace
console.dir: look inside an object
console.dir(element) shows an HTML element as an object with all its properties, while console.log(element) shows it as HTML.
console.clear()
Clears the Console. Many developers also use the clear button at the top of the Console.
A small warning about console.log and objects
When you log an object, the browser keeps a live link. If you expand it later, you see the values as they are now, not as they were when you logged it. To save a snapshot, copy it:
const cart = { items: 1 };
console.log("snapshot:", structuredClone(cart));
cart.items = 5; // the snapshot above still shows items: 1
You will meet
consoleagain, together withalert,promptandconfirm, in the Input and Output lesson.
Step 3: Pause the code with breakpoints
console.log shows you what happened. A breakpoint lets you stop time and look around. When the code reaches a breakpoint, it pauses before running that line, and you can see every variable at that moment.
Setting a breakpoint
- Open DevTools and go to the Sources panel.
- Find your file (press
Ctrl + Pand type the file name). - Click the line number where you want to pause. A blue marker appears.
- Run the action that triggers the code (reload the page or click a button).
- The code pauses and the line is highlighted.
The debugger statement
You can also write the pause into your code:
function calculateTotal(items) {
let total = 0;
for (const item of items) {
debugger; // the code pauses here if DevTools is open
total += item.price;
}
return total;
}
If DevTools is closed, debugger does nothing. Always remove it before publishing your site.
Stepping through code
When the code is paused, use the buttons at the top of the debugger (the keyboard shortcuts are for Chrome):
| Button | Shortcut | What it does |
|---|---|---|
| Resume | F8 |
Continue running until the next breakpoint |
| Step over | F10 |
Run this line. If it calls a function, run the whole function and stop on the next line |
| Step into | F11 |
If this line calls a function, go inside it |
| Step out | Shift + F11 |
Finish the current function and stop where it was called |
| Deactivate breakpoints | Ctrl + F8 |
Turn all breakpoints off temporarily |
A good routine: step over most lines, and step into only when you suspect the function you are about to call.
What to look at while paused
| Section | What you can do |
|---|---|
| Scope | See every variable in the local, closure and global scope, with their current values |
| Watch | Add any expression (like total or items.length) and see its value update at every step |
| Call Stack | See the chain of functions that led here. Click a function to jump to it and see its variables |
| Breakpoints | See, turn on and off, or remove all your breakpoints |
You can also hover over any variable in the code to see its value, and you can type into the Console while paused. The Console runs in the scope of the paused line, so total or item will work there.
Other kinds of breakpoints
| Type | How to set it | Best for |
|---|---|---|
| Conditional breakpoint | Right click a line number, then Add conditional breakpoint, and type a condition like i === 99 |
Stopping on one item in a long loop |
| Logpoint | Right click a line number, then Add logpoint, and type a message | Printing a value without editing your code |
| Event listener breakpoint | Sources panel, Event Listener Breakpoints, tick click |
Finding which code runs on a click |
| DOM breakpoint | Elements panel, right click an element, Break on (subtree, attribute or node removal) | Finding which code changes an element |
| XHR / fetch breakpoint | Sources panel, XHR/fetch Breakpoints, add part of a URL | Pausing when a request is sent |
| Pause on exceptions | Click the pause icon at the top of the Sources panel | Stopping on the exact line that throws an error |
Step 4: The Network panel
Many bugs are not in your code but in the data coming from the server.
- Open the Network panel and reload the page.
- Use the Fetch/XHR filter to see only API calls.
- Click a request to see its details:
| Tab | What it shows |
|---|---|
| Headers | The URL, method (GET, POST), status code, and headers |
| Payload | The data you sent |
| Preview / Response | The data you received (JSON is shown as a tree) |
| Timing | How long each part took |
Reading the status codes
| Code | Meaning | First thing to check |
|---|---|---|
200 |
OK | The request worked. Is the data what you expected? |
301 / 302 |
Redirect | The URL moved |
304 |
Not modified | The browser used its saved copy |
400 |
Bad request | The data you sent is wrong |
401 / 403 |
Not logged in / not allowed | Token, login or permissions |
404 |
Not found | Wrong URL or wrong file path (check spelling and capital letters) |
500 |
Server error | The bug is on the server |
Useful tricks:
- Tick Disable cache (while DevTools is open) to always load the newest files.
- Use the throttling menu (for example Slow 4G or Offline) to test how your site behaves on a slow or broken connection.
- Right click a request and choose Copy, then Copy as fetch, to repeat the same request in code.
Step 5: Inspect the page (Elements panel)
- Inspect any part of the page by right-clicking it and choosing Inspect.
- Change text, attributes and CSS directly. Changes disappear on reload, so they are perfect for experiments.
- In the Styles tab you can tick styles on and off, see which rule wins and which are crossed out.
- Use the Computed tab to see the final value of every property, with the box model diagram.
- Use Event Listeners to see which functions are attached to an element.
- Use the device toolbar (
Ctrl + Shift + M) to test phone and tablet sizes.
The element you selected is available in the Console as $0:
$0.textContent = "Changed from the console!";
$0.style.color = "red";
Handy Console shortcuts
These work only inside the DevTools Console (not in your own files):
| Shortcut | Meaning |
|---|---|
$0 |
The element currently selected in the Elements panel |
$_ |
The result of the previous command |
$("css selector") |
Same as document.querySelector() |
$$("css selector") |
Same as document.querySelectorAll(), returned as a real array |
copy(value) |
Copies a value (for example, a big JSON) to your clipboard |
clear() |
Clears the Console |
Step 6: The Application panel
Open Application to see what your site stores in the browser:
- Local Storage and Session Storage: see, edit and delete keys. Great for checking the JSON you saved in earlier lessons.
- Cookies: see names, values and expiry.
- IndexedDB and Cache Storage: bigger storage used by web apps.
Right click the storage and choose Clear to start fresh while testing.
A step-by-step debugging method
Randomly changing code until it works is slow and risky. Use this method instead:
- Reproduce the bug. Find the exact steps that make it happen every time.
- Read the error message and the line number. If there is no error, describe what you expected and what happened.
- Find where it goes wrong. Add
console.logor set a breakpoint, and check the values step by step. Find the first place where a value is not what you expected. - Form a guess about the cause (“I think
priceis a string”). - Test your guess. Check the value and its type (
typeof). - Fix one thing at a time, and run the code again.
- Test again with other inputs, so the fix does not break something else.
Tip (rubber duck debugging): explain your code line by line, out loud, to a rubber duck or a friend. Very often you find the bug while you explain.
Real-life use cases
1. “My total is wrong”: a bug story
const items = [
{ name: "Pen", price: "10" },
{ name: "Book", price: "120" }
];
let total = 0;
for (const item of items) {
total += item.price;
}
console.log("Total:", total); // "010120" instead of 130
How to find it:
- The result is text, not a number. That is a clue.
- Add
console.log(typeof item.price)inside the loop, or put a breakpoint and watchitem.pricein the Scope panel. - You see that
priceis"10"(text), so+joins text instead of adding. - Fix it by converting:
total += Number(item.price);.
2. Off-by-one error in a loop
const colors = ["red", "green", "blue"];
for (let i = 0; i <= colors.length; i++) { // bug: <= should be <
console.log(i, colors[i]);
}
// The last line prints: 3 undefined
Step through the loop and watch i and colors[i]. You will see colors[3] is undefined, and that i goes one step too far.
3. “Why does the button do nothing?”
Check these in order:
- Console: any red error? (A typo in the file can stop the whole script.)
- Elements: inspect the button, and in Event Listeners check if a
clicklistener is attached. - Sources: put a breakpoint inside the handler. Does it pause when you click?
- Network: if the button calls an API, did the request go out? What status code came back?
4. Finding which code changes an element
Right click the element in Elements, then Break on, then attribute modifications. Now when any script changes it, DevTools pauses on that exact line and the Call Stack shows who did it.
5. Debugging an API call
async function loadUser() {
const response = await fetch("https://jsonplaceholder.typicode.com/users/1");
console.log("status:", response.status, "ok:", response.ok); // check the status first
const user = await response.json();
console.log(user);
}
loadUser();
In the Network panel, click the request, open Preview, and compare the real data with what your code expects. A wrong property name (like user.fullname instead of user.name) is a very common cause of undefined.
6. Log only one item from a long loop
for (let i = 1; i <= 1000; i++) {
if (i === 777) {
console.log("at 777:", i);
}
}
Better: set a conditional breakpoint with i === 777, or a logpoint, so you do not even need to edit the code.
7. Measure which part is slow
console.time("filter");
const result = Array.from({ length: 100000 }, (_, i) => i).filter((n) => n % 2 === 0);
console.timeEnd("filter");
console.log(result.length); // 50000
For bigger problems, record a profile in the Performance panel.
8. Check saved data
If your app forgets its settings, open Application, then Local Storage, and check if the key exists and if the JSON text is valid. A saved value like "undefined" is a classic sign of a bug.
Common mistakes
- Using only
console.log("here"). Add labels and values:console.log("price:", price, typeof price). - Changing many things at once. Change one thing, test, and repeat. Otherwise you will not know what fixed it.
- Not reading the error message. The answer is often in the first line and the line number.
- Leaving
debuggerandconsole.login the final code. Remove them before you publish. - Trusting the logged object’s current look. Expanded objects show the latest values. Log a copy with
structuredClonewhen you need a snapshot. - Editing the page in the Elements panel and expecting it to stay. Changes disappear on reload. Copy useful fixes back into your files.
- Testing with an old cached file. Use Disable cache in the Network panel, or do a hard reload (
Ctrl + Shift + R). - Forgetting the Network panel when data is
undefined. Check what the server really sent. - Ignoring warnings (the yellow messages). They often point to a real bug in the making.
- Debugging a file that is not the one running. Check the file name in the Sources panel. In bigger projects the code may be bundled.
- Giving up and guessing. Use the method: reproduce, locate, guess, test, fix.
Practice
-
Trigger these 3 errors on purpose in the Console, and read each message: an undefined variable,
null.name, andJSON.parse("{ bad"). -
Use
console.tableto print an array of 3 objects. Sort it by clicking a column header. -
Use
console.groupto print a small order with an item list and a payment section. -
Measure the time of a loop with
console.timeandconsole.timeEnd. -
Put a breakpoint inside a function called in a loop. Step through two loops with Step over, and watch one variable in the Watch panel.
-
Write a function with the
debuggerstatement. Open DevTools, run it, and look at the Scope and Call Stack panels. -
Set a conditional breakpoint that only stops when
i === 5in a loop to 10. -
Open the Network panel, reload any website, filter by Fetch/XHR, and find a request with the status
200. Look at its Preview. -
Open any page, select a heading with Inspect, and change its text using
$0.textContentin the Console. -
Open Application, add a key to
localStoragefrom the Console withlocalStorage.setItem("test", "hello"), and find it in the panel. -
Challenge: find the bug in this code without running it first, then confirm with a breakpoint:
function average(numbers) { let sum = 0; for (let i = 0; i <= numbers.length; i++) { sum += numbers[i]; } return sum / numbers.length; } console.log(average([10, 20, 30]));
Recap
- Debugging means finding and fixing the cause of a bug. Use a method: reproduce, locate, guess, test, fix, and change one thing at a time.
- Read the error message. It gives the error type, the reason, and the file and line number. The stack trace shows who called whom.
- Beyond
console.log, useconsole.table,group,timeandtimeEnd,count,assert,trace, anddir. Add labels to your logs. - Breakpoints (click a line number in Sources) and the
debuggerstatement pause the code. Use Resume (F8), Step over (F10), Step into (F11) and Step out (Shift + F11). - While paused, check Scope, Watch and Call Stack, and use the Console. Conditional breakpoints and logpoints save you from editing code.
- The Network panel shows requests, responses and status codes. The Elements panel lets you inspect and change the DOM and CSS live. The Application panel shows
localStorage,sessionStorageand cookies. - In the Console,
$0,$_,$(),$$()andcopy()are handy shortcuts. - Remove
debuggerand extraconsole.logcalls before publishing. - Next you will learn Symbols: a special primitive type for unique values and the hidden keys behind features like
for...of.