Start with the Built-in Inspector
Before reaching for external tools, remember Node.js has a built-in debugger. Run your script with --inspect and open chrome://inspect in Chrome to get a full DevTools experience: breakpoints, step-through, console, and even memory profiling.
node --inspect app.js
For a quick breakpoint without touching the browser, use --inspect-brk to pause on the first line. This is great for debugging startup issues.
Use debugger Statements and Conditional Breakpoints
Sometimes you need a breakpoint only when a condition is true. Instead of littering your code with if blocks, set a conditional breakpoint in DevTools. Right-click the line number, choose "Add conditional breakpoint," and enter an expression like user.id === 42.
For quick inline debugging, debugger; works but remember to remove it before committing. I often use it temporarily when I'm too lazy to open the DevTools UI.
Log Like a Pro with util.inspect
console.log of an object prints [object Object] which is useless. Use util.inspect with depth and colors to see nested structures clearly.
const util = require('util');
console.log(util.inspect(myObject, { showHidden: false, depth: null, colors: true }));
Or in modern Node, you can use console.dir with { depth: null } for the same effect.
Async Stack Traces: Don't Lose the Context
Async errors are painful because stack traces often end at the event loop. Node 12+ gives you better async stack traces by default, but you can improve them further by using Error.captureStackTrace in your own error classes.
class MyError extends Error {
constructor(message) {
super(message);
Error.captureStackTrace(this, MyError);
}
}
This makes the stack trace point to the caller, not the constructor.
Handle Unhandled Rejections and Exceptions
Silent failures are the worst. Set up global handlers to log errors properly and exit gracefully.
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
// Decide whether to crash or continue
});
process.on('uncaughtException', (err) => {
console.error('Uncaught Exception:', err);
// For production, you might want to clean up and exit
});
Use --trace-warnings and --trace-deprecation
When upgrading dependencies, warnings tell you about deprecated APIs. Run with --trace-warnings to see exactly where the warning originates. --trace-deprecation gives you stack traces for deprecated calls, saving you hours of hunting.
node --trace-deprecation app.js
Debug Memory Leaks with Heap Snapshots
If your app's memory grows over time, take heap snapshots in Chrome DevTools (Memory tab). Compare two snapshots taken at different times to see which objects are accumulating. Look for closures that capture large variables or event listeners that are never removed.
Profile CPU Usage
Use the built-in profiler to find hot spots. Run with --prof and then process the output with node --prof-process.
node --prof app.js
node --prof-process isolate-*.log > profile.txt
The text file shows you which functions consume the most CPU. This is invaluable for optimizing performance.
Use node --watch for Development
Node 18+ includes --watch to restart on file changes. Combine it with --inspect to get auto-restart plus debugging.
node --watch --inspect app.js
Debug with Environment Variables
Make your app's behavior configurable via env vars, so you can turn on verbose logging or mock external services without code changes.
const DEBUG = process.env.DEBUG === 'true';
if (DEBUG) console.log('Detailed info');
Use libraries like debug to namespace loggers and filter them via DEBUG=app:*.
Final Thoughts
Mastering these techniques will save you countless hours. Start with the built-in inspector, learn to read stack traces properly, and don't ignore warnings. Debugging is a skill, and the more tools you have, the faster you'll find those pesky bugs.
Top comments (0)