Start with the Right Mindset
Debugging is not about guessing. It's about gathering evidence and narrowing down the problem systematically. In Node.js, the tools are built-in and powerful, but most developers only use console.log. Let's change that.
Use the Built-in Debugger
Node has a built-in debugger that you can start with node inspect. It's a step-by-step command-line debugger. For example:
node inspect app.js
Then you can use commands like cont, next, step, and list. But honestly, the CLI is clunky. The better approach is to use the Chrome DevTools protocol.
Debug with Chrome DevTools
Start your app with --inspect and open chrome://inspect in Chrome. You get a full graphical debugger: breakpoints, watch expressions, call stack, and scope inspection.
node --inspect app.js
For a break at the first line, use --inspect-brk. This is perfect for debugging startup issues.
Use debug for Logging
Instead of sprinkling console.log, use the debug package. It gives namespaced logging that you can turn on/off via the DEBUG environment variable.
const debug = require('debug');
const log = debug('app:server');
const db = debug('app:db');
log('Server starting');
db('Connecting to DB');
Run with DEBUG=app:* node app.js to see all app logs, or DEBUG=app:db to see only DB logs. This keeps your console clean in production.
Handle Unhandled Rejections and Exceptions
Silent failures are the worst. Add global handlers early in your app to catch what you missed.
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
// Application specific logging, throwing an error, or other logic
});
process.on('uncaughtException', (err) => {
console.error('Uncaught Exception:', err);
// Best practice: log and exit, because the app is in an unknown state
process.exit(1);
});
Use node --trace-warnings
If you see warnings about memory or deprecations, run with --trace-warnings to get stack traces for them. This helps identify where the warning originates.
node --trace-warnings app.js
Inspect Memory Leaks
Use the --inspect flag with Chrome DevTools to take heap snapshots. But for quick checks, use process.memoryUsage().
console.log(process.memoryUsage());
For a more detailed look, use the v8 module:
const v8 = require('v8');
console.log(v8.getHeapStatistics());
Use async_hooks for Async Debugging
Async code is hard to trace. The async_hooks module lets you track the lifecycle of async resources. It's advanced but invaluable for finding leaks or lost context.
const async_hooks = require('async_hooks');
const hooks = async_hooks.createHook({
init(asyncId, type, triggerAsyncId) {
console.log(`Init ${type} with id ${asyncId}`);
},
destroy(asyncId) {
console.log(`Destroy ${asyncId}`);
}
});
hooks.enable();
Use it sparingly because it adds overhead.
Use --stack-trace-limit
By default, Node limits stack traces. Increase the limit to see more frames when debugging deep recursion or complex call chains.
node --stack-trace-limit=100 app.js
Use util.inspect for Deep Objects
console.log truncates nested objects. Use util.inspect with full depth.
const util = require('util');
console.log(util.inspect(myObject, { showHidden: false, depth: null, colors: true }));
Debug with ndb (Optional)
ndb is an improved debugging experience for Node.js, built by the Chrome DevTools team. It provides a standalone UI with better navigation. Install it globally and run ndb app.js. It's a nice upgrade over raw Chrome DevTools.
Wrap Up
Stop guessing. Use the built-in tools: --inspect, the debug package, and proper error handlers. These techniques will save you hours and make you a more effective Node.js developer.
Remember: the goal is to understand the problem, not to patch it blindly. Use the tools to see what's happening, then fix the root cause.
Top comments (0)