Debugging Node.js Applications

Difficulty: Intermediate

Debugging is the process of finding and fixing errors in your code. While console.log is the most common debugging technique, Node.js provides powerful built-in debugging tools that let you pause execution, inspect variables, step through code line by line, and evaluate expressions in real time. Mastering these tools dramatically reduces the time spent tracking down bugs.

The Node.js inspector is activated with the `--inspect` flag: `node --inspect app.js`. This starts your application and opens a WebSocket debugging server on port 9229. You can connect to it with Chrome DevTools (navigate to chrome://inspect), VS Code's debugger, or any Chrome DevTools Protocol compatible client. The `--inspect-brk` variant pauses execution on the first line, giving you time to set breakpoints before any code runs.

Chrome DevTools provides a familiar debugging interface for Node.js. After connecting, you get the Sources panel (set breakpoints, step through code), the Console panel (evaluate expressions in the current scope), the Scope panel (inspect local and closure variables), and the Call Stack panel (see the chain of function calls). You can set conditional breakpoints that only pause when a condition is true, and logpoints that log values without pausing.

VS Code's built-in debugger is the most productive option for daily development. Create a launch.json configuration that tells VS Code how to start your app with debugging enabled. You can set breakpoints by clicking the gutter, add watch expressions, inspect the call stack, and hover over variables to see their values. The 'Attach' configuration connects to a running Node.js process started with --inspect, which is useful for debugging servers.

Beyond breakpoint debugging, Node.js offers other diagnostic tools. The `debugger` statement acts as a programmatic breakpoint - when the debugger is attached, execution pauses at that line. Process warnings and unhandled rejection tracking help catch issues early. Memory profiling with the heap snapshot tool identifies memory leaks. CPU profiling shows which functions consume the most time. These tools are essential for diagnosing performance issues in production.

Code examples

Using the Debugger Statement and --inspect

// Save as debug-example.js, run with: node --inspect-brk debug-example.js

function calculateTotal(items) {
  let total = 0;

  for (const item of items) {
    debugger; // Execution pauses here when debugger is attached
    total += item.price * item.quantity;
  }

  return total;
}

const cart = [
  { name: 'Laptop', price: 999, quantity: 1 },
  { name: 'Mouse', price: 29, quantity: 2 },
  { name: 'Keyboard', price: 79, quantity: 1 }
];

const total = calculateTotal(cart);
console.log('Total:', total);

// Run with:
// node --inspect-brk debug-example.js
// Then open chrome://inspect in Chrome
// Click 'inspect' next to your Node.js process
console.log('Debug example ready');
console.log('Run with: node --inspect-brk <filename>');

The debugger statement is a programmatic breakpoint. When you run with --inspect-brk, the process pauses at the first line. When execution reaches the debugger statement, it pauses again. In Chrome DevTools, you can then inspect the item variable, the running total, and step through each iteration.

VS Code Launch Configuration

// .vscode/launch.json configuration for debugging
const launchConfig = {
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Launch Server",
      "type": "node",
      "request": "launch",
      "program": "${workspaceFolder}/src/index.ts",
      "runtimeExecutable": "npx",
      "runtimeArgs": ["tsx"],
      "env": { "NODE_ENV": "development", "PORT": "3000" },
      "console": "integratedTerminal"
    },
    {
      "name": "Attach to Process",
      "type": "node",
      "request": "attach",
      "port": 9229,
      "restart": true
    },
    {
      "name": "Debug Current File",
      "type": "node",
      "request": "launch",
      "program": "${file}",
      "console": "integratedTerminal"
    },
    {
      "name": "Debug Jest Tests",
      "type": "node",
      "request": "launch",
      "runtimeExecutable": "npx",
      "runtimeArgs": ["jest", "--runInBand", "${file}"],
      "console": "integratedTerminal"
    }
  ]
};

console.log('Launch configurations:', launchConfig.configurations.length);
launchConfig.configurations.forEach(c => console.log('-', c.name));

Four common debug configurations: Launch Server starts your Express app with debugging, Attach connects to a running --inspect process, Debug Current File runs the active editor file, and Debug Jest Tests runs tests with the debugger attached. The 'restart' option auto-reconnects when the server restarts.

Debugging Techniques and Tips

// Technique 1: Conditional debugging
function processItems(items) {
  for (const item of items) {
    if (item.price < 0) {
      debugger; // Only useful when this condition is hit
    }
    // In VS Code, set a conditional breakpoint:
    // Right-click gutter -> Add Conditional Breakpoint
    // Condition: item.price < 0
  }
}

// Technique 2: Console methods for quick debugging
function debuggingHelpers() {
  // Measure execution time
  console.time('operation');
  const arr = Array.from({ length: 10000 }, (_, i) => i * 2);
  console.timeEnd('operation');

  // Display tabular data
  const users = [
    { name: 'Alice', role: 'admin', active: true },
    { name: 'Bob', role: 'user', active: false }
  ];
  console.table(users);

  // Group related logs
  console.group('User Processing');
  console.log('Loading users...');
  console.log('Found 2 users');
  console.groupEnd();

  // Assertion (logs error only if condition is false)
  console.assert(arr.length === 10000, 'Array length mismatch!');
  console.assert(arr.length === 5, 'This will log an error');
}

// Technique 3: Unhandled rejection tracking
process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection:', reason);
  // In production: log to monitoring service, then gracefully shutdown
});

process.on('uncaughtException', (error) => {
  console.error('Uncaught Exception:', error.message);
  // In production: log, cleanup, exit with code 1
  // process.exit(1);
});

console.log('Debugging techniques demonstrated');

Three debugging approaches: conditional breakpoints for targeted pausing, console utility methods for quick inspection, and process event handlers to catch unhandled errors. The unhandledRejection handler is essential - without it, unhandled promise rejections are silently ignored in older Node.js versions.

Key points

Concepts covered

--inspect, Chrome DevTools, VS Code debugger, breakpoints, debugger statement, node --inspect-brk