TechLead
Lesson 11 of 20
5 min read
DevTools & Productivity

Debugging Node.js

Debug Node.js applications with the built-in inspector, VS Code debugger, and production debugging techniques

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://inspect and 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-brk pauses before any code runs.
  • Skip node_modules: Always configure skipFiles in 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.

Continue Learning