Debugging with DevTools

JavaScript · Lesson 57 of 61 9 min read Updated

Every programmer writes bugs, every day. What separates beginners from experienced developers is not fewer bugs but a calm, repeatable way of finding them. Your most important tool is already installed: every modern browser has DevTools, a set of panels for inspecting and debugging the page.

In this lesson you will open DevTools, read error messages properly, use more of the console than console.log(), pause code with breakpoints and step through it line by line, and check network requests. You will practise on two small bugs that beginners hit all the time.

Opening DevTools

Press F12, or Ctrl+Shift+I on Windows and Linux (Cmd+Option+I on a Mac). You can also right-click anything on a page and choose Inspect. DevTools has several panels; these are the ones you will use most:

PanelWhat it is for
Elements (Inspector in Firefox)The live DOM and its CSS, including changes made by JavaScript
ConsoleMessages, errors, and a place to type and run JavaScript
Sources (Debugger in Firefox)Your script files, breakpoints and step-by-step running
NetworkEvery request the page makes, with status, timing and response
Application (Storage in Firefox)localStorage, sessionStorage, cookies and other stored data

The exact names and layout differ a little between Chrome, Edge, Firefox and Safari, but the ideas are the same everywhere. In Safari, first turn on the developer features in Safari's settings.

Read the error message

When a script fails, the Console shows a red message such as:

Text
Uncaught TypeError: Cannot read properties of null (reading 'addEventListener')
    at app.js:12:31

Beginners often skim past this, but it tells you nearly everything. Read it in three parts:

  1. The type, TypeError: you used a value the wrong way.
  2. The message: something before .addEventListener was null.
  3. The location, app.js:12:31: file, line and column. Click it to jump to that exact line in the Sources panel.

Now ask "why is that value null?" Here, querySelector found nothing, perhaps because of a typo in the selector or because the script ran before the element existed (see The DOM: Selecting Elements).

These messages (shown as Chrome writes them) cover a large share of beginner bugs:

MessageUsual cause
x is not definedA typo in a variable name, or the variable is out of scope
Cannot read properties of undefined (reading 'name')The thing before .name is undefined: a wrong key, or data not loaded yet
x.getName is not a functionThe method name is misspelt, or the object is not what you think
Assignment to constant variable.You assigned a new value to a const
missing ) after argument list, Unexpected tokenA bracket, quote or comma is missing or extra

Debugging with the console

The quickest technique is to print values at key points and compare them with what you expected. Run this example. The average should be about 85, but it prints NaN:

JavaScriptRun it yourself
function average(list) {
  let total = 0;
  for (let i = 0; i <= list.length; i++) {
    console.log(`i = ${i}, value = ${list[i]}, total so far = ${total}`);
    total += list[i];
  }
  return total / list.length;
}

console.log(`Ananya's average: ${average([72, 88, 95])}`);
Output
i = 0, value = 72, total so far = 0
i = 1, value = 88, total so far = 72
i = 2, value = 95, total so far = 160
i = 3, value = undefined, total so far = 255
Ananya's average: NaN

The log shows the loop running a fourth time with i = 3, where list[3] is undefined, and 255 + undefined is NaN. The condition should be i < list.length. This is called an off-by-one error, one of the most common bugs of all. Notice the log message includes labels: i = 3 is far more useful than a bare 3.

The console has more than log:

JavaScript
console.table([{ name: "Aarav", marks: 78 }, { name: "Isha", marks: 91 }]); // a neat table
console.error("Payment failed");    // red, with a stack trace
console.warn("Stock is low");       // yellow warning
console.count("render");            // "render: 1", "render: 2"... counts calls
console.time("load");               // start a stopwatch...
console.timeEnd("load");            // ...and print how long it took
console.assert(total >= 0, "Total should never be negative"); // logs only if false
console.group("Cart");              // indent the following messages...
console.groupEnd();                 // ...until here

In the Console panel you can also type any expression and press Enter to run it on the live page. In Chrome and Firefox, $0 refers to the element currently selected in the Elements panel.

Pausing with breakpoints

Adding logs means editing, saving and reloading. A breakpoint is often faster: it pauses the code at a line so you can look at every variable at that moment.

To set one, open the Sources panel, open your file and click the line number. Or write the statement debugger; in your code: when DevTools is open, the browser pauses there. While paused you can:

  • Hover over any variable to see its value.
  • Read the Scope section, which lists all local and outer variables.
  • Add Watch expressions, such as total + 1, that update as you step.
  • Read the Call Stack, which shows how the code got here.

Then move through the code with the step buttons (the shortcuts below are Chrome's):

ButtonShortcutWhat it does
ResumeF8Run until the next breakpoint
Step overF10Run the current line, without going inside function calls
Step intoF11Go inside the function called on this line
Step outShift+F11Finish the current function and stop in its caller

Try it on this page. It should add two prices, but it shows a strange total:

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Buggy bill</title>
</head>
<body>
  <h1>Meera's Boutique, Lucknow</h1>
  <p><label>Kurta ₹ <input type="number" value="499"></label></p>
  <p><label>Dupatta ₹ <input type="number" value="120"></label></p>
  <button type="button" id="calc">Calculate total</button>
  <p id="total"></p>

  <script>
    document.querySelector("#calc").addEventListener("click", () => {
      const inputs = document.querySelectorAll("input");
      let total = 0;
      for (const input of inputs) {
        debugger; // pauses here, but only while DevTools is open
        total += input.value;
      }
      document.querySelector("#total").textContent = `Total: ₹${total}`;
    });
  </script>
</body>
</html>

Without DevTools open, the page shows "Total: ₹0499120". Open DevTools and click the button again. The code pauses at debugger. Hover over input.value: it is "499", a string with quotes. Press F10 to step once, and total becomes "0499". The + joined text instead of adding numbers. The fix is total += Number(input.value); (or input.valueAsNumber), as you learned in Working with Forms. Remove the debugger line once the bug is fixed.

You can also pause on events: Event Listener Breakpoints in the Sources panel pause when, for example, any click handler starts. That helps when you cannot tell which code is running.

Checking network requests

When data does not appear, open the Network panel and repeat the action. Click the request to see its status (200, 404, 500...), the request headers and payload you sent, and the response the server returned. Many "JavaScript bugs" turn out to be a wrong URL, a 404, or a server returning an HTML error page instead of JSON. Turn on Preserve log to keep requests across page reloads, and use the throttling menu to test on a slow connection. See Fetch API.

A method that works

  1. Reproduce the bug reliably. Note the exact steps.
  2. Read the error message and its location.
  3. Shrink the problem: remove code until you have the smallest example that still fails.
  4. Guess a cause, then check it with a log or a breakpoint. Do not change code based on a guess alone.
  5. Fix one thing at a time and test again.

Explaining the code aloud, line by line, to a friend or even a rubber duck on your desk, is surprisingly effective. Saying what each line should do often reveals the line that does not.

Common mistakes

  • Ignoring the console. Keep DevTools open while you develop, and read every red message.
  • Changing many things at once. If the bug disappears, you will not know which change fixed it.
  • Assuming instead of checking. "That variable must be a number" is exactly the kind of belief that hides bugs. Look at the value.
  • Leaving debugger or noisy logs in finished code. Remove them before you publish.

Key points

  • Open DevTools with F12 or Ctrl+Shift+I (Cmd+Option+I on a Mac).
  • Read every error in three parts: type, message, and the file and line link.
  • Use labelled console.log calls and other console methods to compare actual values with expected ones.
  • Breakpoints and debugger; pause the code so you can inspect variables and step line by line.
  • Check the Network panel for status codes and responses when data goes missing.
  • Reproduce, read, shrink, check, then fix one thing at a time.

Exercise

This function should return the total runs for a batting card, but it returns "0452311". Use console.log (or a breakpoint) to find out why, then fix it.

JavaScript
function totalRuns(scores) {
  let total = 0;
  for (const score of scores) {
    total += score.runs;
  }
  return total;
}
console.log(totalRuns([{ runs: "45" }, { runs: "23" }, { runs: "11" }]));
Show solution

Logging typeof score.runs inside the loop prints string three times: the runs were stored as text, so += joins them. Convert each value before adding:

JavaScript
function totalRuns(scores) {
  let total = 0;
  for (const score of scores) {
    total += Number(score.runs);
  }
  return total;
}
console.log(totalRuns([{ runs: "45" }, { runs: "23" }, { runs: "11" }])); // 79

Data that comes from forms, storage or some APIs is often text, so check its type when maths gives strange results.

Quick Quiz

1. An error says at app.js:27:5. What does 27:5 tell you?

2. What does the debugger; statement do?

3. While paused, which action runs the current line without going inside the functions it calls?

4. A list should show data from an API but stays empty. Where should you look first?

Learn JavaScript with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call