In short
Debugging JavaScript with browser DevTools means finding and fixing bugs using the console, breakpoints, stepping, watch expressions and the Network tab. Learn the common error messages too. Opening DevTools and using console methods Every modern browser has DevTools (developer tools).
Debugging JavaScript with browser DevTools means finding and fixing bugs using the console, breakpoints, stepping, watch expressions and the Network tab. Learn the common error messages too.
Opening DevTools and using console methods
Every modern browser has DevTools (developer tools). Press F12, or Ctrl+Shift+I (Cmd+Option+I on a Mac). The examples here use Chrome and Edge, but Firefox and Safari have the same main features.
You already know console.log(). The console has more useful methods.
const users = [
{ name: 'Ana', age: 28, role: 'admin' },
{ name: 'Ben', age: 34, role: 'editor' },
];
console.table(users); // shows a neat table with columns
console.warn('Low stock: 2 items'); // yellow warning
console.error('Payment failed'); // red error with a stack trace
console.group('Checkout'); // start a collapsible group
console.log('Step 1: cart');
console.log('Step 2: address');
console.groupEnd();
console.time('loop');
for (let i = 0; i < 100_000; i++) {}
console.timeEnd('loop'); // Output: loop: 1.2 ms (your time will differ)
console.count('clicked'); // Output: clicked: 1
console.count('clicked'); // Output: clicked: 2
console.assert(users.length > 5, 'Expected more than 5 users');
// Output: Assertion failed: Expected more than 5 users
console.table() is great for arrays of objects. console.group() keeps related logs together. console.assert() only prints when the condition is false.
Tip: Log objects with a label, like
console.log('user:', user)orconsole.log({ user }). The second form shows the variable name too, so you know which value you are looking at.
How to use breakpoints and the debugger statement
console.log is fine, but a breakpoint is more powerful. It pauses your code at a line, like pressing pause on a video, so you can look at every variable at that moment.
To set one: open the Sources panel (called Debugger in Firefox), find your file, and click the line number. A blue marker appears. Run the code, and it stops there.
You can also pause from inside your code with the debugger statement.
<button id="addBtn">Add to cart</button>
<p id="total">Total: $0</p>
<script>
const cart = [];
function calculateTotal(items) {
let total = 0;
for (const item of items) {
debugger; // DevTools pauses here when open
total += item.price * item.qty;
}
return total;
}
document.querySelector('#addBtn').addEventListener('click', () => {
cart.push({ name: 'Book', price: '12', qty: 2 }); // bug: price is a string
document.querySelector('#total').textContent = `Total: $${calculateTotal(cart)}`;
});
</script>
Open DevTools, then click the button. The code pauses on debugger. Hover over item and you see price: '12' is a string. Here it still works because * converts strings to numbers, but if you later change it to total += item.price, you would get "012". Breakpoints help you see these things.
Common mistake: Forgetting
debuggerin your code. It does nothing for normal users, but remove it before you publish, as it will pause the page for anyone who has DevTools open.
Other breakpoint types
- Conditional breakpoint: right-click a line number and pick "Add conditional breakpoint", for example
item.qty > 10. It only pauses when the condition is true. - Logpoint: right-click and pick "Add logpoint" to print a message without editing your code.
- Event listener breakpoints: in the Sources panel, pause on any
clickorsubmit, even if you don't know where the handler is.
Stepping through code and watch expressions
When the code is paused, the toolbar has step buttons:
| Button | What it does |
|---|---|
| Resume (F8) | Continue running until the next breakpoint |
| Step over (F10) | Run the current line, then stop on the next one |
| Step into (F11) | Go inside the function called on this line |
| Step out (Shift+F11) | Finish the current function and stop where it was called |
On the right side you will see Scope (all variables available right now) and Call Stack (the chain of functions that led here). In the Watch section, click + and type any expression, like total or item.price * item.qty. Its value updates every time you step. This is how you watch a bug happen line by line.
Using the Network tab to fix fetch problems
When fetch() does not work, open the Network tab and reload the page. Click the request to see its details.
- Status:
200is OK,404means wrong URL,401or403means not allowed,500means the server crashed. - Headers: check the exact URL and method that was sent.
- Payload: the data you sent with POST.
- Response or Preview: what the server actually returned.
async function loadUser() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/users/9999');
if (!response.ok) {
throw new Error(`Server answered ${response.status}`);
}
const user = await response.json();
console.log(user.name);
} catch (error) {
console.error('Could not load user:', error.message);
}
}
loadUser();
// Output: Could not load user: Server answered 404
In the Network tab you will see this request in red with status 404. Remember: fetch() does not throw for a 404 or 500 by itself, so always check response.ok.
Common JavaScript error messages explained
| Error message | What it usually means | How to fix it |
|---|---|---|
TypeError: x is not a function | You called something that isn't a function, often a typo or wrong method name | Check spelling, e.g. addEventListner, or log typeof x |
TypeError: Cannot read properties of null (reading 'addEventListener') | querySelector found nothing | Check the selector, or load the script with defer so the HTML exists |
TypeError: Cannot read properties of undefined (reading 'name') | The object before .name is missing | Log the object; data may not have loaded yet; consider ?. |
ReferenceError: total is not defined | The variable does not exist here | Check spelling and scope (was it declared inside another block?) |
SyntaxError: Unexpected token | A typo in the code: missing bracket, comma or quote | Click the file link to jump to the line, and look just before it |
Uncaught (in promise) | An async error that nobody caught | Add try...catch around your await code |
Failed to fetch or a CORS error | Network problem, or the server does not allow your site | Check the URL in the Network tab; CORS must be fixed on the server |
The file name and line number next to each error is a link. Click it to jump straight to the problem.
Practice exercise
Take this buggy code: const prices = [10, 20, 30]; let sum; for (let i = 0; i <= prices.length; i++) { sum += prices[i]; } console.log(sum);. It prints NaN. Put a breakpoint inside the loop, add sum and prices[i] as watch expressions, and step until you find both bugs.
Hint: look at the starting value of sum and at what prices[i] is on the last loop.
Key takeaways
- Use
console.table,console.group,console.timeandconsole.assertfor clearer logs. - Breakpoints and the
debuggerstatement pause code so you can inspect every variable. - Step over, into and out, and use watch expressions to follow a bug line by line.
- The Network tab shows the status, request and response of every
fetchcall. - Most errors have a common cause: read the message, click the line link, and check what is null or undefined.