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
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
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);
}
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();
});
});
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
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);
});
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');
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();
});
}
Profile CPU Usage
Node's built-in profiler gives you a CPU profile without extra dependencies.
node --prof app.js
This writes a .log file. Then process it:
node --prof-process isolate-*.log > processed.txt
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
}
The Power of --trace-warnings
Deprecation warnings can be silent. Run with --trace-warnings to see where they originate.
node --trace-warnings app.js
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)