Debugging

Every developer spends a large share of their time finding out why code doesn't do what they expected. Debugging is a skill you can learn: read error messages carefully, inspect values instead of guessing, and use the browser's developer tools to pause code and look around.

Read the error message

Error messages are more helpful than they first look. Take this one:

Console
Uncaught TypeError: Cannot read properties of undefined (reading 'name')
    at renderUser (app.js:14:28)
    at loadPage (app.js:31:3)
  • Type — TypeError: a value was used the wrong way.
  • Message — something was undefined, and the code tried to read .name from it.
  • Stack trace — it happened in renderUser, at line 14, column 28 of app.js, which was called from loadPage at line 31.

So: on line 14, the thing before .name is undefined. Now ask why — what was supposed to set it? Clicking the file link in the console jumps straight to the line.

Common messages decoded

MessageUsual cause
X is not definedTypo in a name, a missing import, or using a variable outside its scope
Cannot read properties of undefined / nullA missing object: unloaded data, wrong property name, querySelector found nothing
X is not a functionCalling something that isn't a function — typo in a method name, or wrong type of value
Unexpected tokenSyntax error: missing bracket, comma or quote nearby
Assignment to constant variableReassigning a const
Cannot access 'X' before initializationUsing a let/const before its declaration line
Maximum call stack size exceededInfinite recursion — a function calling itself with no stopping condition

Logging with purpose

console.log debugging works, but make the output informative:

JS logging.js
const user = { id: 7, name: "Ana", roles: ["editor"] };
const cart = [{ sku: "A1", qty: 2 }];

console.log({ user, cartSize: cart.length });
console.log("roles:", JSON.stringify(user.roles));
Output
{ user: { id: 7, name: 'Ana', roles: [ 'editor' ] }, cartSize: 1 }
roles: ["editor"]

Wrapping variables in braces — console.log({ user }) — prints their names alongside their values. In the browser, console.table displays arrays of objects as a table, console.trace() prints how the current line was reached, and console.count("label") counts how often a line runs.

Watch out: browser consoles show objects "live". If you log an object and then change it, expanding it in the console may show the new values. Log structuredClone(obj) or JSON.stringify(obj) to capture a snapshot.

Breakpoints

A breakpoint pauses code at a line so you can inspect everything at that moment. In Chrome or Edge DevTools:

  1. Open DevTools (F12) and go to the Sources panel (Debugger in Firefox).
  2. Open your file and click a line number to set a breakpoint.
  3. Trigger the code (reload, click the button…). Execution pauses at that line.
  4. Hover over variables to see their values, or check the Scope pane.
  5. Step through with the toolbar: Step over (next line), Step into (enter a function call), Step out (finish the current function), Resume.

You can also pause from code with the debugger; statement, which acts as a breakpoint whenever DevTools is open:

JS debugger.js
function applyDiscount(order) {
  debugger; // execution pauses here when DevTools is open
  return order.total * (1 - order.discount);
}

Other useful breakpoint types: conditional breakpoints (right-click a line number — pause only when i === 99), logpoints (log without editing the code), event listener breakpoints (pause on any click), and DOM breakpoints (pause when an element changes).

The Network panel

For anything involving fetch, the Network panel shows every request: its URL, method, status code, headers, the body you sent and the response you got back. Many "my data isn't showing" bugs turn out to be a 404, a CORS error, or a response with a different shape than expected — all visible here in seconds.

A debugging process

  1. Reproduce the bug reliably. Note the exact steps.
  2. Read the error and stack trace, if there is one.
  3. Form a hypothesis — "I think items is empty here".
  4. Test it with a log or breakpoint, rather than changing code and hoping.
  5. Narrow it down: comment out halves of the code, or use git bisect to find which change introduced the bug.
  6. Fix the cause, not the symptom, then check the original steps work.

Explaining the problem out loud, line by line — to a colleague or even a rubber duck — is surprisingly effective, because it forces you to check assumptions you'd otherwise skip.

Practice: find the bug

This function should return the average, but prints the wrong value. Add a console.log inside the loop to find out why, then fix it:

Try it yourself
Console output
Show the fix

The loop starts at i = 1, skipping the first number. Arrays start at index 0, so it should be for (let i = 0; …) — or avoid indexes entirely with numbers.reduce((a, b) => a + b, 0).