Node.js Debugging Overview
Node.js includes a powerful built-in debugging protocol that integrates with Chrome DevTools and VS Code. Unlike browser JavaScript where you can simply open DevTools, Node.js debugging requires connecting an inspector client to the running Node.js process. This topic covers every approach from simple console debugging to production-grade observability.
The Node.js Inspector
# Start Node.js with the inspector enabled
node --inspect src/server.ts # Inspector on port 9229
node --inspect-brk src/server.ts # Same but pauses on first line
node --inspect=0.0.0.0:9229 server.js # Allow remote connections
# For TypeScript with tsx
npx tsx --inspect src/server.ts
# For Next.js
NODE_OPTIONS='--inspect' next dev
# Open chrome://inspect in Chrome to connect
# Or use the dedicated DevTools URL printed in the terminal
Inspector Connection Methods
- Chrome DevTools: Navigate to
chrome://inspectand click "Open dedicated DevTools for Node" - VS Code: Use the built-in debugger with a launch.json configuration (recommended)
- WebStorm: Built-in Node.js debugging support with run configurations
- ndb: Google's improved debugging experience:
npx ndb node server.js
VS Code Debugging Configuration
VS Code provides the best Node.js debugging experience. Create a .vscode/launch.json file to configure debug profiles.
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Current File",
"type": "node",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
"skipFiles": ["/**", "node_modules/**"]
},
{
"name": "Debug Next.js",
"type": "node",
"request": "launch",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"],
"console": "integratedTerminal",
"serverReadyAction": {
"pattern": "- Local:.+(https?://.+)",
"uriFormat": "%s",
"action": "debugWithChrome"
}
},
{
"name": "Debug Jest Tests",
"type": "node",
"request": "launch",
"runtimeExecutable": "npx",
"runtimeArgs": [
"jest",
"--runInBand",
"--watchAll=false",
"${relativeFile}"
],
"console": "integratedTerminal"
},
{
"name": "Debug Vitest",
"type": "node",
"request": "launch",
"runtimeExecutable": "npx",
"runtimeArgs": ["vitest", "run", "${relativeFile}"],
"console": "integratedTerminal"
},
{
"name": "Attach to Process",
"type": "node",
"request": "attach",
"port": 9229,
"restart": true,
"skipFiles": ["/**"]
}
]
}
Debugging Techniques
Debugging API Routes
// Set breakpoints in your API route handlers
// VS Code will pause when a request hits the breakpoint
// Next.js API Route (app/api/users/route.ts)
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const query = searchParams.get('q');
debugger; // Pause here to inspect the request
const users = await db.user.findMany({
where: { name: { contains: query ?? '' } },
});
return Response.json(users);
}
// Express route
app.get('/api/users', async (req, res) => {
debugger; // Inspect req.query, req.headers, etc.
try {
const users = await User.find(req.query);
res.json(users);
} catch (error) {
debugger; // Inspect the error
res.status(500).json({ error: 'Internal server error' });
}
});
Debugging with Environment Variables
# Enable verbose logging for specific modules
DEBUG=express:* node server.js # Express debug logs
DEBUG=knex:query node server.js # Knex SQL query logs
NODE_DEBUG=http,net node server.js # Node.js internal HTTP/net logs
NODE_DEBUG=module node server.js # Module resolution debugging
# Increase memory for large apps
NODE_OPTIONS="--max-old-space-size=4096" node server.js
# Enable source maps for stack traces in production
NODE_OPTIONS="--enable-source-maps" node dist/server.js
Debugging Memory Issues
# Take a heap snapshot from a running process
# Send SIGUSR2 to the Node.js process
kill -USR2 $(pgrep -f "node server.js")
# Generate a heap snapshot programmatically
node --inspect server.js
# Then in Chrome DevTools Memory panel, take a snapshot
# Monitor memory usage in code
setInterval(() => {
const usage = process.memoryUsage();
console.log({
rss: (usage.rss / 1024 / 1024).toFixed(2) + ' MB',
heapUsed: (usage.heapUsed / 1024 / 1024).toFixed(2) + ' MB',
heapTotal: (usage.heapTotal / 1024 / 1024).toFixed(2) + ' MB',
external: (usage.external / 1024 / 1024).toFixed(2) + ' MB',
});
}, 10000);
Production Debugging
// Structured logging for production debugging
import pino from 'pino';
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
transport: process.env.NODE_ENV === 'development'
? { target: 'pino-pretty' }
: undefined,
});
// Add request context to all logs
app.use((req, res, next) => {
req.log = logger.child({
requestId: req.headers['x-request-id'] || crypto.randomUUID(),
method: req.method,
path: req.path,
userAgent: req.headers['user-agent'],
});
next();
});
// Use structured logging everywhere
app.get('/api/users', async (req, res) => {
req.log.info({ query: req.query }, 'Fetching users');
try {
const users = await db.user.findMany();
req.log.info({ count: users.length }, 'Users fetched successfully');
res.json(users);
} catch (error) {
req.log.error({ error }, 'Failed to fetch users');
res.status(500).json({ error: 'Internal server error' });
}
});
Node.js Debugging Tips
- Use --inspect-brk for startup issues: If the bug occurs during initialization,
--inspect-brkpauses before any code runs. - Skip node_modules: Always configure
skipFilesin launch.json to avoid stepping into third-party code. - Use conditional breakpoints for API debugging: Set conditions like
req.query.userId === '42'to only break on specific requests. - Attach to running processes: Use the "Attach" configuration to debug already-running Node processes without restarting them.