Effective debugging is a critical skill for Node.js developers.
While console.log() is useful for basic debugging, advanced techniques allow you to diagnose complex issues like memory leaks, performance bottlenecks, and race conditions.
This tutorial covers advanced debugging techniques and tools to help you solve challenging problems in your Node.js applications.
Advanced debugging tools provide capabilities like:
Setting breakpoints and stepping through code execution
Inspecting variable values at runtime
Visualizing memory consumption and finding leaks
Profiling CPU usage to identify performance bottlenecks
Analyzing asynchronous call stacks
Debugging with Chrome DevTools
Node.js includes built-in support for the Chrome DevTools debugging protocol, allowing you to use the powerful Chrome DevTools interface to debug your Node.js applications.
Starting Node.js in Debug Mode
There are several ways to start your application in debug mode:
Standard Debug Mode
node --inspect app.js
This starts your app normally but enables the inspector on port 9229.
Break on Start
node --inspect-brk app.js
This pauses execution at the first line of code, allowing you to set up breakpoints before execution begins.
Custom Port
node --inspect=127.0.0.1:9222 app.js
This uses a custom port for the inspector.
Connecting to the Debugger
After starting your Node.js application with the inspect flag, you can connect to it in several ways:
Chrome DevTools: Open Chrome and navigate to chrome://inspect.
You should see your Node.js application listed under "Remote Target."
Click "inspect" to open DevTools connected to your application:
Chrome DevTools for Node.js
DevTools URL: Open the URL shown in the terminal
(usually something like devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=127.0.0.1:9229/...).
Using DevTools for Debugging
Once connected, you can use the full power of Chrome DevTools:
Sources Panel: Set breakpoints, step through code, and watch variables
Call Stack: View the current execution stack, including async call chains
Scope Variables: Inspect local and global variables at each breakpoint
Console: Evaluate expressions in the current context
Memory Panel: Take heap snapshots and analyze memory usage
Pro Tip: Use the Sources panel's "Pause on caught exceptions" feature (the pause button with curved lines) to automatically break when an error occurs.
Debugging in VS Code
Visual Studio Code provides excellent built-in debugging capabilities for Node.js applications.
Setting Up Node.js Debugging in VS Code
You can start debugging your Node.js application in VS Code in several ways:
launch.json Configuration: Create a .vscode/launch.json file to define how VS Code should launch or attach to your application.
Auto-Attach: Enable auto-attach in VS Code settings to automatically debug any Node.js process started with the --inspect flag.
JavaScript Debug Terminal: Use the JavaScript Debug Terminal in VS Code to automatically debug any Node.js process started from that terminal.
Breakpoints: Set, disable, and enable breakpoints by clicking in the gutter of your code editor.
Conditional Breakpoints: Right-click on a breakpoint to set a condition that must be true for the breakpoint to trigger.
Logpoints: Add logging without modifying code by setting logpoints that print messages to the console when hit.
Watch Expressions: Monitor the value of variables and expressions as you step through code.
Call Stack: View and navigate the call stack, including asynchronous frames.
Note: VS Code can also debug TypeScript files directly, with source maps enabling debugging of the original TypeScript code rather than the transpiled JavaScript.
Using the Debug Module
The debug module is a lightweight debugging utility that allows you to add conditional logging to your Node.js applications without cluttering your code with console.log statements.
Installing the Debug Module
npm install debug
Basic Usage of Debug
The debug module lets you create namespaced debug functions that can be enabled or disabled via environment variables:
Example: Using the Debug Module
// Create namespaced debuggers for different parts of your application
const debug = require('debug');
// Use the debuggers in your code
debugServer('Server starting on port %d', 8080);
debugDatabase('Connected to database: %s', 'mongodb://localhost');
debugAuth('User %s authenticated', 'john@example.com');
// By default, these debug messages won't appear in the output
Enabling Debug Output
To see the debug output, set the DEBUG environment variable to a comma-separated list of namespace patterns:
Enable All Debug Output
DEBUG=app:* node app.js
Enable Specific Namespaces
DEBUG=app:server,app:auth node app.js
Enable All but Exclude Some
DEBUG=app:*,-app:database node app.js
Debug Output Features
Each namespace has a unique color for easy visual identification
Timestamps show when each message was logged
Supports formatted output similar to console.log
Shows the difference in milliseconds from the previous log of the same namespace
Best Practice: Use specific namespaces for different components of your application to make it easier to filter debug output based on what you're currently troubleshooting.
Finding and Fixing Memory Leaks
Memory leaks in Node.js applications can cause performance degradation and eventual crashes.
Detecting and fixing memory leaks is a crucial debugging skill.
Common Causes of Memory Leaks in Node.js
Global Variables: Objects stored in global scope that are never cleaned up
Closures: Functions that maintain references to large objects or variables
Event Listeners: Listeners that are added but never removed
Caches: In-memory caches that grow without bounds
Timers: Timers (setTimeout/setInterval) that aren't cleared
Promises: Unhandled promises or promise chains that never resolve
Detecting Memory Leaks
Several approaches can help you detect memory leaks:
Heap snapshots provide a detailed view of memory allocation:
Start your app with node --inspect app.js
Connect with Chrome DevTools
Go to the Memory tab
Take heap snapshots at different points
Compare snapshots to find objects that are growing in number or size
3. Use Memory Profiling Tools
clinic doctor: Identify memory issues in your application
clinic heap: Visualize heap memory usage
memwatch-next: Library to detect memory leaks
Example: Memory Leak in a Node.js Server
Here's an example showing a common memory leak pattern in a Node.js server:
const http = require('http');
// This object will store data for each request (memory leak!)
const requestData = {};
const server = http.createServer((req, res) => {
// Generate a unique request ID
const requestId = Date.now() + Math.random().toString(36).substring(2, 15);
// Store data in the global object (THIS IS THE MEMORY LEAK)
requestData[requestId] = {
url: req.url,
method: req.method,
headers: req.headers,
timestamp: Date.now(),
// Create a large object to make the leak more obvious
payload: Buffer.alloc(1024 * 1024) // Allocate 1MB per request
};
// Log memory usage after each request
const memoryUsage = process.memoryUsage();
console.log(`Memory usage after request ${requestId}:`);
console.log(`- Heap Used: ${Math.round(memoryUsage.heapUsed / 1024 / 1024)} MB`);
console.log(`- Request count: ${Object.keys(requestData).length}`);
// Store data in the global object
requestData[requestId] = {
url: req.url,
method: req.method,
timestamp: Date.now()
};
// Clean up after the response is sent (FIX FOR THE MEMORY LEAK)
res.on('finish', () => {
delete requestData[requestId];
console.log(`Cleaned up request ${requestId}`);
});
// Convert callbacks to promises
const readFile = util.promisify(fs.readFile);
// Function with a nested chain of async operations
async function processUserData(userId) {
try {
console.log(`Processing data for user ${userId}...`);
// Fetch user data
const userData = await fetchUserData(userId);
console.log(`User data retrieved: ${userData.name}`);
// Get user posts
const posts = await getUserPosts(userId);
console.log(`Retrieved ${posts.length} posts for user`);
// Process posts (this will cause an error for userId = 3)
const processedPosts = posts.map(post => {
return {
id: post.id,
title: post.title.toUpperCase(),
contentLength: post.content.length, // Will fail if content is undefined
};
});
return { user: userData, posts: processedPosts };
} catch (error) {
console.error('Error processing user data:', error);
throw error;
}
}
// Simulated API call
function fetchUserData(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (userId <= 0) {
reject(new Error('Invalid user ID'));
} else {
resolve({ id: userId, name: `User ${userId}` });
}
}, 500);
});
}