Complete JavaScript Course Module 16: Advanced: Professional JavaScript

Debugging JavaScript with Browser DevTools

  • Beginner
  • Lesson 16.1
  • 6 min read
  • 71 of 76 in the course

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) or console.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 debugger in 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 click or submit, 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:

ButtonWhat 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: 200 is OK, 404 means wrong URL, 401 or 403 means not allowed, 500 means 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 messageWhat it usually meansHow to fix it
TypeError: x is not a functionYou called something that isn't a function, often a typo or wrong method nameCheck spelling, e.g. addEventListner, or log typeof x
TypeError: Cannot read properties of null (reading 'addEventListener')querySelector found nothingCheck 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 missingLog the object; data may not have loaded yet; consider ?.
ReferenceError: total is not definedThe variable does not exist hereCheck spelling and scope (was it declared inside another block?)
SyntaxError: Unexpected tokenA typo in the code: missing bracket, comma or quoteClick the file link to jump to the line, and look just before it
Uncaught (in promise)An async error that nobody caughtAdd try...catch around your await code
Failed to fetch or a CORS errorNetwork problem, or the server does not allow your siteCheck 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.time and console.assert for clearer logs.
  • Breakpoints and the debugger statement 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 fetch call.
  • Most errors have a common cause: read the message, click the line link, and check what is null or undefined.