NodeJSDebugging Node.js

Debugging Node.js

Debugging is the systematic process of finding why code behaves differently than you expect — and Node gives you far more than console.log. There's a full V8 debugger built into the runtime, breakpoint debugging in VS Code and Chrome DevTools, and diagnostic tooling for crashes and performance. This page sets the foundation: a disciplined debugging method, the spectrum of techniques from logging to breakpoints, why console.log debugging hits a wall, and the categories of bug you'll meet in Node — including the async-specific ones that trip everyone up.

A method, not random poking
  • Reproduce it reliably — a bug you can't trigger on demand, you can't fix with confidence. Find the minimal input that reproduces it.

  • Form a hypothesiswhat do you think is wrong and why? Debugging is the scientific method, not guesswork.

  • Observe the actual state — inspect variables, arguments, and control flow at the point of failure; don't assume.

  • Isolate — narrow the failing code by bisecting: comment out, add checks, or git bisect across commits.

  • Fix the root cause, not the symptom — a patch that hides the bad value leaves the real bug in place.

  • Add a regression test — reproduce the bug as a failing test first, so it can never silently return.

Reproduce → hypothesize → inspect → isolate → fix → test — debugging is a discipline
The difference between hours of frustration and a five-minute fix is usually *method*. Start by making the bug reproducible — ideally a single command or test that triggers it every time. Then treat each guess as a hypothesis you actively test by observing real state, rather than changing code hopefully and re-running. Bisecting (`git bisect` to find the commit that introduced it, or commenting out halves of the code) narrows the search space fast. Once fixed, capture it as a regression test so the bug stays dead.
The debugging toolkit — from cheap to powerful

Technique

Best for

Limitation

console.log

Quick, one-off checks

No history, clutters code, requires re-runs to add more

util.inspect / console.dir

Inspecting deep/nested objects

Still manual logging

Built-in debugger (node inspect)

Breakpoints from the terminal

CLI ergonomics are basic

Inspector + DevTools/VS Code

Full breakpoint debugging, watch, call stack

Setup needed; not for prod by default

Diagnostic report / heap snapshot

Crashes, leaks, production incidents

Post-hoc analysis, not live stepping

Why console.log debugging hits a wall

JS
// The console.log spiral — add a log, re-run, add another, re-run...
function processOrder(order) {
  console.log('order:', order)              // is the input right?
  const total = calculateTotal(order.items)
  console.log('total:', total)              // is the total right?
  const tax = total * 0.2
  console.log('tax:', tax)                  // ...and so on, forever
  return total + tax
}
`console.log` debugging is slow and leaves landmines — breakpoints inspect everything at once
Print debugging works for trivial cases, but it scales badly: each new question means editing code and re-running, you only see the values you *remembered* to print, and forgotten `console.log`s leak into commits and production output. A **breakpoint** pauses execution and lets you inspect *every* variable in scope, walk the entire call stack, evaluate expressions, and step line-by-line — all without changing the code or re-running. For anything beyond a quick check, the built-in [inspector](/nodejs/node-inspector) with [VS Code](/nodejs/vscode-debugging) or [Chrome DevTools](/nodejs/chrome-devtools) is dramatically faster. Reserve `console.log` for the cases where it's genuinely quicker.
Better than bare console.log when you must log

JS
import util from 'node:util'

const obj = { user: { id: 1, roles: ['admin'] }, nested: { deep: { value: 42 } } }

// console.log truncates deep objects to [Object]:
console.log(obj)                    // { user: ..., nested: { deep: [Object] } }

// util.inspect shows full depth + colour:
console.log(util.inspect(obj, { depth: null, colors: true }))

// console.table for arrays of objects:
console.table([{ id: 1, name: 'Ada' }, { id: 2, name: 'Linus' }])

// console.trace prints the call stack to here:
console.trace('how did we get here?')

// Time a block:
console.time('query'); await db.query(sql); console.timeEnd('query')  // query: 12.4ms
`util.inspect({ depth: null })` reveals deeply nested objects `console.log` collapses to `[Object]`
When you do log, use the right tool. `console.log` collapses nested objects beyond two levels to `[Object]`; `util.inspect(obj, { depth: null })` shows the whole structure. `console.table` renders arrays of objects as a readable grid, `console.trace` prints the call stack from the current point (great for "who called this?"), and `console.time`/`timeEnd` give quick timing. These built-ins make logging far more informative than a bare `console.log` — but they're still a stopgap compared to breakpoints.
Categories of Node bug

Category

Typical cause

Tool

Logic errors

Wrong condition, off-by-one, bad assumption

Breakpoints, step-through

Async bugs

Missing await, unhandled rejection, race condition

Async stack traces, careful logging

Crashes

Uncaught exception, out-of-memory

Diagnostic report, core dumps

Memory leaks

Retained references, growing caches

Performance

Blocked event loop, slow queries

Profiling, flame graphs

Async bugs hide the real stack — a missing `await` often shows an error far from its cause
Node's asynchronous model creates a bug class that's uniquely confusing: a forgotten `await` lets execution continue past an operation that hasn't finished, so the error surfaces *later* and *elsewhere* than where it originated, often with a stack trace that doesn't include your code at all. Unhandled promise rejections, race conditions between concurrent operations, and callbacks firing in unexpected order are the usual suspects. Enable async stack traces (on by default in modern Node), always `await` your promises, and lean on breakpoints inside async functions where stepping reveals the real order of execution.
Next
Start with the debugger built into the runtime: [The Built-in Debugger & Inspector](/nodejs/node-inspector).