Debugging Node. js Like a Pro
Node.js developers often fall back on console.log for debugging, but this article shows how to use the built‑in inspector, conditional breakpoints, async stack traces, and profiling tools to diagnose issues more reliably. It covers starting a process with --inspect, using debugger statements, loggi…
When a Node.js application crashes, the first instinct is to sprinkle console.log statements throughout the code. That approach can work for simple bugs, but it quickly breaks down when asynchronous code hides the real source of the problem, when a variable changes between logging and the error, or when a server misbehaves only on a rare request. The solution is to use the debugging and profiling tools that come with Node.js, which provide real call stacks, conditional breakpoints, and performance insights.
Start with --inspect, not print statements
The Node.js inspector is a powerful debugging interface that lets you set breakpoints, step through code, and view a real call stack. To launch your application with the inspector, run:
node --inspect app.js
Open Chrome, navigate to chrome://inspect, and click "inspect" to connect. If you cannot restart the process easily—such as a worker or a Docker container—use --inspect-brk to pause on the first line or send SIGUSR1 to enable the inspector on the fly.
When debugging from outside a container, bind the inspector to a publicly reachable address, but avoid exposing it on a public host:
node --inspect=0.0.0.0:9229 app.js
Break on the condition, not the line
Setting a plain breakpoint inside a hot loop can cause the debugger to pause thousands of times, making it hard to find the real issue. Instead, add a conditional breakpoint. In Chrome DevTools, right‑click the line and choose "Add conditional breakpoint". For example:
- Instead of
for (const order of orders) { processOrder(order); }, set the conditionorder.id === 'abc123'so the debugger stops only once when the target order is processed.
Use debugger statements deliberately
Placing debugger; in the code behaves like a breakpoint when the inspector is attached but is ignored otherwise. This makes it safe to leave temporary debugging statements in the code during a session and remove them before committing:
function applyDiscount(cart) { debugger; return cart.total * 0.9; }
Log objects, not strings
Printing objects with console.log('user:', user) often results in [object Object] or truncated output. Use JSON.stringify with indentation or console.dir with unlimited depth:
console.log(JSON.stringify(user, null, 2));console.dir(user, { depth: null });
The depth: null option handles circular references that would otherwise throw.
Trace async properly with --async-stack-traces
Async stack traces are enabled by default in recent Node.js versions, but older releases may need the flag:
node --async-stack-traces app.js
Without this, errors thrown inside setTimeout or Promise chains show a stack that starts at the callback, not at the code that scheduled it. With the flag, you see the full causal chain, making it easier to pinpoint the source of the error.
Attach a profiler when it’s slow, not broken
For performance issues, breakpoints are ineffective. Use the built‑in profiler:
node --cpu-prof --cpu-prof-dir=./profiles app.js
The generated .cpuprofile file can be loaded into Chrome DevTools under the Performance tab. Wide bars indicate functions consuming the most time. For memory profiling, use --heap-prof or periodically log process.memoryUsage():
setInterval(() => { const { heapUsed } = process.memoryUsage(); console.log(`heap: ${(heapUsed / 1024 / 1024).toFixed(1)} MB`); }, 5000);
A steadily rising heap suggests a memory leak, while sawtooth patterns indicate normal garbage collection.
Read the error, including the code
Node errors expose structured fields such as err.code and err.port. Handling these codes gives immediate insight into the failure category. For example:
server.on('error', err => { if (err.code === 'EADDRINUSE') { console.error(`Port ${err.port} is taken`); } console.error(err); });
Logging the entire error object instead of just err.message preserves context and stack information.
A quick checklist
- Reproduce the issue reliably before debugging.
- Use conditional breakpoints to avoid excessive pauses.
- Start with
--inspect-brkfor startup crashes, or sendSIGUSR1to running processes. - Log objects with
console.dir(..., { depth: null })instead of string concatenation. - Profile CPU and memory with
--cpu-profand--heap-proffor performance problems. - Check
err.codebefore inspectingerr.message.
All of these tools are built into Node.js. The key is remembering they exist and using them before adding dozens of console.log statements.
Why it matters
Effective debugging reduces development time and prevents subtle bugs from reaching production. Leveraging Node.js’s built‑in tools leads to faster issue resolution and more maintainable code.
Key points
- Use the inspector with --inspect or --inspect-brk for real call stacks
- Add conditional breakpoints to avoid pausing on every loop iteration
- Insert debugger statements that only trigger when the inspector is attached
- Log objects with console.dir and unlimited depth for complete visibility
- Enable async stack traces to see the full causal chain
- Profile CPU and memory with --cpu-prof and --heap-prof to diagnose performance issues
- Always check err.code for quick error categorization
Frequently asked questions
How do I enable the inspector on a running process?
Send SIGUSR1 to the process or use the --inspect-brk flag to pause on the first line.
What is the difference between console.log and console.dir?
console.log prints a string representation, which can truncate objects, while console.dir prints a full object structure with optional depth control.
When should I use --cpu-prof instead of breakpoints?
Use --cpu-prof when the issue is performance‑related, such as slow response times, because breakpoints cannot capture execution time.




