DEV Community

Stack Horizon
Stack Horizon

Posted on

Debugging Node.js Like a Pro

Start with the Built-in Inspector

Before reaching for external tools, remember Node.js ships with a powerful debugger. Run your script with --inspect and open chrome://inspect in Chrome (or any Chromium browser). You get breakpoints, step-through, watch expressions, and the console right there.

node --inspect app.js
Enter fullscreen mode Exit fullscreen mode

For an even easier start, use --inspect-brk to pause on the first line so you can set breakpoints before any code runs.

node --inspect-brk app.js
Enter fullscreen mode Exit fullscreen mode

Use the Debugger Statement

Sometimes you want a quick breakpoint without touching the browser. Drop a debugger; statement in your code. When you run with --inspect, execution pauses there automatically.

function calculateTotal(items) {
  debugger; // pause here
  return items.reduce((sum, item) => sum + item.price, 0);
}
Enter fullscreen mode Exit fullscreen mode

This is perfect for a quick look at variables without setting up formal breakpoints.

Async Debugging with AsyncLocalStorage

Async code is where bugs hide. The context gets lost across promises and callbacks. AsyncLocalStorage from node:async_hooks lets you track request IDs or user context across async boundaries.

const { AsyncLocalStorage } = require('node:async_hooks');
const storage = new AsyncLocalStorage();

function log(message) {
  const context = storage.getStore();
  console.log(`[${context?.requestId ?? 'no-id'}] ${message}`);
}

app.use((req, res, next) => {
  storage.run({ requestId: req.headers['x-request-id'] }, () => {
    next();
  });
});
Enter fullscreen mode Exit fullscreen mode

Now your logs carry context even when async operations interleave. You'll spot which request caused the error.

Read the Stack Trace Properly

Node stack traces can be noisy. Use the --stack-trace-limit flag to increase the number of frames shown.

node --stack-trace-limit=100 app.js
Enter fullscreen mode Exit fullscreen mode

But more importantly, filter out internal Node modules. Look for the first line that references your own code. That's where the bug is, not in node:internal files.

Handle Unhandled Rejections Explicitly

Silent promise rejections are the worst. In modern Node (>=15), unhandled rejections crash by default, but you might still miss the root cause if you don't log properly.

process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection at:', promise, 'reason:', reason);
  process.exit(1);
});
Enter fullscreen mode Exit fullscreen mode

Add this early in your app and you'll catch the exact promise that failed.

Use Logging Levels, Not Just console.log

console.log is fine for quick checks, but for real debugging you need levels. Use pino or winston. They give you timestamps, levels, and structured output that you can grep.

const pino = require('pino');
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });

logger.debug('Entering function');
logger.info({ userId: 123 }, 'User logged in');
logger.error({ err }, 'Failed to save');
Enter fullscreen mode Exit fullscreen mode

Then run with LOG_LEVEL=debug when you need more detail, without changing code.

Debug Memory Leaks with Heap Snapshots

If your app's memory grows over time, take heap snapshots. Run your app with --inspect, then in Chrome DevTools go to the Memory tab and take a snapshot. Compare two snapshots taken at different times to see what objects are accumulating.

You can also trigger a snapshot programmatically:

const inspector = require('node:inspector');
const fs = require('node:fs');

function takeSnapshot() {
  const session = new inspector.Session();
  session.connect();
  session.post('HeapProfiler.takeHeapSnapshot', (err, { result }) => {
    if (err) return console.error(err);
    fs.writeFileSync('heap.heapsnapshot', result.data);
    session.disconnect();
  });
}
Enter fullscreen mode Exit fullscreen mode

Profile CPU Usage

Node's built-in profiler gives you a CPU profile without extra dependencies.

node --prof app.js
Enter fullscreen mode Exit fullscreen mode

This writes a .log file. Then process it:

node --prof-process isolate-*.log > processed.txt
Enter fullscreen mode Exit fullscreen mode

Open the processed file and look for functions with high self-time. Those are your hot spots.

Test With Realistic Input

Many bugs only appear with real data. Use fixtures that mimic production. If you're dealing with JSON, use JSON.parse with a try-catch to see malformed data.

try {
  const data = JSON.parse(raw);
} catch (err) {
  console.error('Bad JSON:', raw.slice(0, 200)); // log a snippet
}
Enter fullscreen mode Exit fullscreen mode

The Power of --trace-warnings

Deprecation warnings can be silent. Run with --trace-warnings to see where they originate.

node --trace-warnings app.js
Enter fullscreen mode Exit fullscreen mode

This often reveals outdated API usage that will break in future versions.

Wrap Up

Debugging is a skill. Start with the built-in inspector for step-by-step, use AsyncLocalStorage for async context, and get comfortable with stack traces and heap snapshots. Add structured logging early, and you'll spend less time guessing and more time fixing.

Remember: the best debugging is the kind you avoid by writing clear, testable code. But when bugs do appear, these tools will help you find them fast.

Top comments (0)