Skip to content
elephantoo

Debugging JavaScript

Lesson 31 of 34 14 min read

console power tools, breakpoints, the debugger statement, stack traces and debugging Node.


Every developer spends a large part of their time finding out why code doesn't do what they expected. Debugging is a skill you can learn: reproduce the problem, read the error, form a hypothesis, inspect the actual values, and fix the cause rather than the symptom. This lesson covers the tools — the console, breakpoints and DevTools in the browser and in Node.js — and a process to use them.

Step 1: read the error message#

JavaScript
function totalPrice(cart) {
  return cart.items.reduce((sum, item) => sum + item.price * item.qty, 0);
}

try {
  totalPrice({});
} catch (err) {
  console.log(err.name);    // TypeError
  console.log(err.message); // Cannot read properties of undefined (reading 'reduce')
}

The message tells you exactly what happened: something was undefined when we tried to read .reduce on it — so cart.items is undefined. The stack trace below it (in the console or terminal) gives the file and line, plus each function call that led there. Read it top-down; the first line from your code (not a library) is usually where to look.

MessageUsual cause
x is not definedTypo, missing import, wrong scope
Cannot read properties of undefined (reading 'y')The thing before .y is undefined — trace back where it came from
x is not a functionWrong name, or the value isn't what you think (e.g. a promise)
Unexpected tokenSyntax error, often a missing bracket or invalid JSON
Assignment to constant variableReassigning a const
Maximum call stack size exceededInfinite recursion

The console, beyond console.log#

JavaScript
const user = { name: "Ada", roles: ["admin"], address: { city: "London" } };

console.log("user:", user);          // label your logs
console.log({ user });               // shorthand: shows the variable name too
console.info("Info message");
console.warn("Deprecated option");   // yellow, with a stack trace in browsers
console.error("Something failed");   // red, goes to stderr in Node

console.table([
  { name: "Ada", score: 91 },
  { name: "Alan", score: 78 },
]);

console.group("Checkout");
console.log("validating cart");
console.log("charging card");
console.groupEnd();

console.count("render"); // render: 1
console.count("render"); // render: 2

console.time("loop");
for (let i = 0; i < 1e6; i++);
console.timeEnd("loop"); // loop: 1.2ms (varies)

console.assert(user.roles.includes("editor"), "user should be an editor"); // logs only if false
console.trace("How did we get here?"); // prints the current call stack
Output
┌─────────┬────────┬───────┐
│ (index) │ name   │ score │
├─────────┼────────┼───────┤
│ 0       │ 'Ada'  │ 91    │
│ 1       │ 'Alan' │ 78    │
└─────────┴────────┴───────┘

Live objects: browser consoles show a reference to logged objects. If the object changes after logging, expanding it later shows the new values. To log a snapshot, use console.log(structuredClone(obj)) or JSON.stringify(obj, null, 2).

Breakpoints: pause and look around#

Logging means guessing where to look and re-running. A breakpoint pauses the program so you can inspect everything at that moment.

In Chrome/Edge DevTools (Firefox is very similar):

  1. Open DevTools (F12) → Sources tab (Firefox: Debugger).
  2. Open your file (Ctrl+P / Cmd+P to search).
  3. Click a line number to set a breakpoint.
  4. Trigger the code (reload, click the button…). Execution pauses on that line.

While paused you can:

  • Hover variables to see their values, or check the Scope panel (local, closure and global variables).
  • Look at the Call Stack to see how you got here — click a frame to inspect its variables.
  • Type expressions in the Console; they run in the paused scope.
  • Add Watch expressions that update as you step.

Then control execution:

ButtonShortcutDoes
ResumeF8Run until the next breakpoint
Step overF10Run the current line, don't go into function calls
Step intoF11Go into the function called on this line
Step outShift+F11Finish the current function and return to the caller

Smarter breakpoints

  • Conditional breakpoint: right-click a line number → Add conditional breakpoint → item.price === undefined. It only pauses when the condition is true — ideal inside loops.
  • Logpoint: right-click → Add logpoint → "item", item. Logs without editing your code.
  • DOM breakpoints: in the Elements tab, right-click a node → Break on → subtree modifications / attribute modifications / node removal. Finds the code that changes an element.
  • Event listener breakpoints: Sources → Event Listener Breakpoints → e.g. Mouse → click.
  • Pause on exceptions: the ⏸ button with the stop sign pauses whenever an error is thrown — even caught ones if you tick the option.
  • XHR/fetch breakpoints: pause when a request URL contains a given string.

The debugger statement

JavaScript
function applyDiscount(price, code) {
  debugger; // pauses here when DevTools is open
  const rate = code === "SAVE10" ? 0.1 : 0;
  return price * (1 - rate);
}

Handy when it's hard to find the line in DevTools (e.g. bundled code). Never commit it — linters flag it.

Other DevTools panels you'll use#

  • Network: every request with status, headers, payload and response, plus timing. The first stop for "the data isn't showing" bugs. Throttle to Slow 4G to test loading states.
  • Elements: inspect and edit the live DOM and CSS; see which event listeners are attached.
  • Application/Storage: inspect localStorage, sessionStorage, cookies, IndexedDB and service workers.
  • Performance: record what the page does over time to find slow functions and layout thrashing.
  • Memory: heap snapshots to track down leaks.
  • Lighthouse: audits for performance, accessibility and SEO.

Source maps#

Production code is bundled and minified, so stack traces point to index-a1b2c3.js:1:48211. Source maps (.map files generated by Vite, esbuild, TypeScript…) let DevTools show and break on your original source files. Vite enables them in development automatically; enable build.sourcemap if you need them in production.

Debugging Node.js#

Run with the inspector

Terminal
node --inspect-brk app.js

Then open chrome://inspect in Chrome and click inspect — you get the same Sources panel and breakpoints for Node code. --inspect-brk pauses on the first line; --inspect starts running immediately.

VS Code

VS Code has a built-in JavaScript debugger. Open the Run and Debug panel and choose Node.js, or open a JavaScript Debug Terminal — any node or npm run command started there is debugged automatically, and breakpoints set in the editor just work.

Watch mode and useful flags

Terminal
node --watch app.js          # restart automatically when files change
node --trace-warnings app.js # show where warnings come from
node --env-file=.env app.js  # load environment variables from a file

A debugging process that works#

  1. Reproduce it reliably. Write down the exact steps. A bug you can't reproduce, you can't confirm fixed.
  2. Read the error and stack trace carefully.
  3. Form a hypothesis: "I think items is undefined because the API returns data.items".
  4. Inspect actual values with a breakpoint or log at the point you suspect — and check your assumptions about types (typeof, Array.isArray).
  5. Narrow it down. Comment out half the code, or use git bisect to find the commit that broke it. Binary search is fast.
  6. Fix the cause, not the symptom. Adding ?. everywhere silences an error; finding out why the value is missing fixes the bug.
  7. Add a test so it can't come back.

Explaining the problem out loud — to a colleague or a rubber duck — often reveals the answer before you finish the sentence.

Classic bugs to check first#

JavaScript
// 1. String vs number
console.log("2" + 2);         // "22" – input values are strings

// 2. Missing return in a callback with braces
console.log([1, 2].map((n) => { n * 2; })); // [ undefined, undefined ]

// 3. Async value used too early
const p = Promise.resolve(42);
console.log(p);               // Promise { 42 } – forgot to await

// 4. Mutation surprise
const original = { tags: ["a"] };
const copy = { ...original };
copy.tags.push("b");
console.log(original.tags);   // [ 'a', 'b' ] – shallow copy shared the array

// 5. Off-by-one
const arr = [10, 20, 30];
console.log(arr[arr.length]); // undefined – last index is length - 1

Common mistakes#

  • Changing code randomly until it works, without understanding why.
  • Leaving console.log and debugger statements in committed code — use a linter rule to catch them.
  • Debugging minified code without source maps.
  • Ignoring warnings in the console — they often predict tomorrow's errors.

What's next#

Next: Node.js and npm — running JavaScript outside the browser, using built-in modules, installing packages and writing npm scripts.

Check your understanding

Quick quiz

0/3 answered
  1. 1.What does the debugger; statement do when DevTools is open?

  2. 2.Which console method shows an array of objects as a sortable table?

  3. 3.You logged an object, expanded it later in the console, and it shows values that changed *after* the log. Why?

Finished reading?

Mark this lesson complete to track your progress.