Debugging With console Methods and the VS Code Debugger

8 min read

console.log is the universal debugging tool, and you'll keep using it forever. But it's the opening of your debugging toolkit, not the whole thing. This lesson covers the specialised console methods that make output structured and scannable — and the VS Code debugger, which gives you full pause-and-inspect powers without scattering print statements through your code.

The console.log family

console is a global object with several methods, each tuned for a different shape of output. You've been using log since lesson 3 of chapter 1; here are the others.

console.error — for failures

Prints in red (in browsers and most terminals) and is sent to stderr in Node — meaning CI logs and pipes can separate it from normal output.

console.error("Test failed:", { name: "login", status: "fail" });

Use it for actual failures. Save red for real signals; using console.error for routine output dilutes its meaning.

console.warn — for non-fatal concerns

Yellow output. Reserve it for "this works but you should look at it" — slow responses, deprecated APIs, retries that should not have been needed.

console.warn("Response slow:", responseTime, "ms (SLA 2000)");

console.table — for arrays of objects

The single biggest upgrade from console.log. console.table renders an array of objects as a real table — one column per property, one row per object. Indispensable for inspecting test data and API responses.

const users = [
  { name: "Alice", role: "admin",  active: true  },
  { name: "Bob",   role: "member", active: false },
  { name: "Carol", role: "guest",  active: true  }
];
console.table(users);

Output:

┌─────────┬─────────┬──────────┬────────┐
│ (index) │  name   │   role   │ active │
├─────────┼─────────┼──────────┼────────┤
│    0    │ 'Alice' │ 'admin'  │  true  │
│    1    │  'Bob'  │ 'member' │ false  │
│    2    │ 'Carol' │ 'guest'  │  true  │
└─────────┴─────────┴──────────┴────────┘

Far more readable than three separate console.log lines, and instant comprehension when scanning a long fixture.

console.time / console.timeEnd — for measuring

Wrap a block of code between matching time and timeEnd calls (matched by label). Node prints the elapsed time at the timeEnd call.

console.time("seed");
seedDatabase();
console.timeEnd("seed");

Output:

seed: 124.527ms

Useful for debugging slow tests, validating a performance fix, or sanity-checking that an await you "thought" was parallel is actually sequential (chapter 5).

console.group / console.groupEnd — for nesting

Groups related log lines under a collapsible header. In Node, the children are simply indented; in browsers, you get a real expandable group.

console.group("Test: login");
console.log("Step 1: navigate");
console.log("Step 2: submit");
console.warn("Step 3: slow response");
console.groupEnd();

Output:

Test: login
  Step 1: navigate
  Step 2: submit
  Step 3: slow response

Wrap each test case's logs in a group and a long output becomes easy to scan.

When to use which method

(Equal bars — the visual is a labelled cheat-sheet of methods, not a comparison of importance. Pick whichever method's meaning matches your message.)

The case for a real debugger

console.log works, but at some point you need to:

  • Inspect a complex object three layers deep without printing the whole thing.
  • Watch a value change as your loop iterates.
  • Pause execution at a specific line and explore the state interactively.
  • Step through code one statement at a time to understand a flow.

That's what a debugger is for. VS Code ships with a built-in debugger that's vastly more powerful than printing — and once you've used it, you'll never go back for non-trivial bugs.

Setting a breakpoint

The fastest way in:

  1. Open any .js file in VS Code.
  2. Click in the gutter — the empty space just left of a line number. A red dot appears. That's a breakpoint.
  3. Open the Run and Debug panel (the bug+play icon in the sidebar, or Ctrl+Shift+D).
  4. Click "Run and Debug" → choose "Node.js" if prompted. VS Code launches the file in debug mode.
  5. Execution pauses at your breakpoint. The line is highlighted; the Variables panel shows everything in scope.

A reusable launch configuration lives in .vscode/launch.json. The minimum:

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Debug current file",
      "program": "${file}"
    }
  ]
}

Save that file once and you can launch the debugger on whichever file is open with F5.

Stepping through code

When execution is paused at a breakpoint, four shortcuts move you through the code one statement at a time:

  • F5 — Continue. Run until the next breakpoint (or the end of the program).
  • F10 — Step Over. Run the current line, then pause. If the line calls a function, run the whole function and stop on the next line — don't go inside.
  • F11 — Step Into. Run the current line, but if it calls a function, enter that function and pause on its first line.
  • Shift+F11 — Step Out. Finish the current function and pause where it was called.

A typical debugging session: set a breakpoint near where the bug seems to live, F5 to run there, F10 to walk past lines you trust, F11 when you reach a suspicious function call, until you find the line where the variable is wrong. The whole loop is faster than peppering the file with console.log and re-running.

Watch expressions

The Variables panel shows everything in scope, but you'll often want to track a specific expression — say, users.filter(u => u.active).length. The Watch panel evaluates expressions every time execution pauses.

In the Watch panel, click +, paste the expression, and as you step through code the value updates each time you stop. Indispensable when you're trying to identify the exact moment a value goes wrong.

A real debugging session

Imagine a test data generator that should return 10 unique emails but returns 7 unique and 3 duplicates. Steps to debug:

  1. Set a breakpoint inside the loop where each email is generated.
  2. Run with F5.
  3. Pause on the first iteration. Inspect the variables — note the seed value, the timestamp, the index.
  4. Add seenEmails.size to the Watch panel. Step through with F10. After 7 iterations, watch the size stop growing — that's where duplicates start.
  5. Inspect why — likely the timestamp resolution is too coarse, or the index is being reused. The bug is now visible without a single console.log.

The skill compounds. Five minutes with a debugger saves an hour of print-debugging on a hard bug, and it scales to bugs console.log can never reach (race conditions, deep object inspection, conditional breakpoints).

⚠️ Common mistakes

  • Leaving console.log calls in committed code. Linters can flag them; pre-commit hooks can fail on them. A test suite with hundreds of stray logs hides the ones that actually matter.
  • Using console.log when console.table would be far clearer. A 20-row array of objects scrolls past as opaque output with log and is instantly readable with table. Reach for the structured method when the data is structured.
  • Skipping the debugger because "console.log is faster." It is — for the first two minutes. After that, the structured power of pause-and-inspect wins decisively. Spend 30 minutes setting up a debugger config you trust; the time is recovered on the first hard bug.

🎯 Practice task

Two parts — the console methods, then the debugger. 25-30 minutes.

Part A — console methods (10 min):

  1. In your js-for-qa folder, create inspect.js:

    const users = [
      { name: "Alice", role: "admin",  active: true,  loginsLast30d: 12 },
      { name: "Bob",   role: "member", active: false, loginsLast30d: 0  },
      { name: "Carol", role: "guest",  active: true,  loginsLast30d: 3  }
    ];
     
    console.group("User audit");
    console.table(users);
    console.time("filter");
    const inactive = users.filter(u => !u.active);
    console.timeEnd("filter");
    console.warn("Inactive users:", inactive.length);
    if (inactive.length > 0) console.error("Audit failed");
    console.groupEnd();
  2. Run node inspect.js. Note how the table, timing, and group nesting render.

Part B — debugger (15-20 min):

  1. In VS Code, create .vscode/launch.json with the snippet above.
  2. Set a breakpoint on the const inactive = users.filter(...) line.
  3. Press F5 to run in debug mode.
  4. When it pauses, inspect the Variables panel. Hover over users to see the array.
  5. Add users.filter(u => u.active).length to the Watch panel. Step over with F10. Confirm the watch updates.
  6. Stretch: modify the script to call a function audit(user) inside the filter. Set a breakpoint inside audit. Use F11 to step into it from the filter call. Use Shift+F11 to step back out. You've just traced execution through a higher-order function, which is impossible to do cleanly with print statements.

The final lesson of this chapter is a reference card — the ten errors you'll meet most often, and how to fix each one fast.

// tip to track lessons you complete and pick up where you left off across devices.