DEV Community

Alex Chen
Alex Chen

Posted on

Debugging Node.js Like a Pro: My Debugging Workflow (2026)

Debugging Node.js Like a Pro: My Debugging Workflow (2026)

Stop using console.log everywhere. Here's my systematic approach to finding bugs fast.

The Debugging Mindset

Before touching code:
1. What exactly is the symptom? (Not your guess — the actual behavior)
2. When does it happen? (Every time? Intermittent? Under load?)
3. What changed recently? (Code, deps, environment, data)

The bug is never where you think it is.
The log message that will reveal it is never the one you want to write.
Enter fullscreen mode Exit fullscreen mode

Level 1: Console (But Smarter)

// ❌ SCATTERSHOT debugging
console.log('here');
console.log('data:', data);
console.log('after function');

// ✅ STRUCTURED logging
const debug = require('debug')('myapp:user');
debug('Creating user with email=%s', email);
// Set DEBUG=myapp:* to see these, empty string to hide
Enter fullscreen mode Exit fullscreen mode
// ✅ NAMED console groups
console.group('🔍 Auth Flow');
  console.log('Token received:', token.substring(0, 10) + '...');
  console.log('Expires at:', new Date(expires * 1000));
  console.groupDecoded('Payload');
    console.log(decoded);
  console.groupEnd();
console.groupEnd();
Enter fullscreen mode Exit fullscreen mode
// ✅ TABLE for objects/arrays
console.table(users.map(u => ({ id: u.id, email: u.email, role: u.role })));

// ✅ COUNTER for loops
let i = 0;
items.forEach(item => {
  console.count('processed'); // auto-increments each call
  process(item);
});
console.countReset('processed'); // reset when done
Enter fullscreen mode Exit fullscreen mode
// ✅ TIME for performance
console.time('db-query');
const users = await db.query('SELECT * FROM users');
console.timeEnd('db-query'); // → db-query: 142ms

// ✅ TRACE for call stack
function deepFunction() {
  console.trace('How did I get here?'); // Shows full stack trace
}
Enter fullscreen mode Exit fullscreen mode
// ✅ ASSERT for invariants
const assert = require('node:assert/strict');
assert(user.email.includes('@'), 'Email should contain @', { email: user.email });
assert(response.status < 400, `Unexpected status: ${response.status}`);
Enter fullscreen mode Exit fullscreen mode

Level 2: Node.js Built-in Debugger

# Start debugger (breaks on first line)
node inspect server.js

# Or break on first statement of a script
node --inspect-brk script.js

# Attach to running process
node --inspect=9229 server.js
# Then connect from Chrome: chrome://inspect
Enter fullscreen mode Exit fullscreen mode

Debugger Commands

c(ontinue)        # Resume execution
n(ext)            # Step over (don't enter functions)
s(tep)           # Step into (enter functions)
o(ut)            # Step out of current function
pa(use)          # Pause code execution
re(sum)          # Resume execution
Enter fullscreen mode Exit fullscreen mode

Programmatic Breakpoints

// Conditional breakpoint — only stops when condition is true
debugger; // Always stops

if (userId === 'problematic-user-123') {
  debugger; // Only stops for this user
}

if (data.length > 10000) {
  debugger; // Stops when data looks suspicious
}
Enter fullscreen mode Exit fullscreen mode

Level 3: VS Code Debugger (Game Changer)

launch.json Configuration

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Launch Server",
      "program": "${workspaceFolder}/server.js",
      "cwd": "${workspaceFolder}",
      "env": { "NODE_ENV": "development" },
      "runtimeArgs": ["--inspect"],
      "console": "integratedTerminal",
      "restart": true,
      "skipFiles": ["<node_internals>/**"]
    },
    {
      "type": "node",
      "request": "attach",
      "name": "Attach to Running",
      "port": 9229,
      "restart": true,
      "skipFiles": ["<node_internals>/**"]
    },
    {
      "type": "node",
      "request": "launch",
      "name": "Run Tests",
      "program": "${workspaceFolder}/node_modules/.bin/node",
      "args": ["--test", "--test-reporter", "spec", "${workspaceFolder}/tests"],
      "console": "integratedTerminal"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Must-Know Features

Watch Expressions: Monitor variables that update in real-time as you step.

Call Stack Panel: See exactly how you got here. Click any frame to see local variables at that point.

Breakpoint Conditions: Right-click breakpoint → "Edit Breakpoint" → Add condition:

users.length > 50  // Only breaks when array has > 50 items
email.includes('test')  // Only breaks for test emails
i === 42  // Only breaks on specific iteration
Enter fullscreen mode Exit fullscreen mode

Logpoints: Non-breaking breakpoints that log messages:

Right-click line number → "Add Logpoint"
Enter: "User ID: {userId}, Role: {role}"
→ Shows in debug console without pausing execution!
Enter fullscreen mode Exit fullscreen mode

Inline Values: Enable in VS Code settings:

{
  "debug.inlineValues": "on"  // Shows variable values inline in editor
}
Enter fullscreen mode Exit fullscreen mode

Level 4: Async Debugging

This is where most developers get stuck:

// ❌ Can't see inside this easily
async function processData() {
  const data = await fetch(url);     // What's in data?
  const parsed = await data.json();   // And here?
  const result = await transform(parsed); // ???
  return result;
}

// ✅ Break INSIDE async functions
async function processData() {
  const data = await fetch(url);     
  debugger; // ← Inspect data here
  const parsed = await data.json();   
  debugger; // ← Inspect parsed here  
  const result = await transform(parsed); 
  return result;
}

// ✅ Or use .then() chain for stepping
fetch(url)
  .then(r => r.json())
  .then(data => { debugger; return data; }) // Breaks here
  .then(transform);
Enter fullscreen mode Exit fullscreen mode

Promise rejection handling:

// Catch ALL unhandled rejections
process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection at:', promise, 'reason:', reason);
  // Don't crash in dev, but log everything
  if (process.env.NODE_ENV === 'production') {
    process.exit(1);
  }
});

// In tests or critical paths:
await expectAsync(() => somePromise()).resolves.toBeDefined();
Enter fullscreen mode Exit fullscreen mode

Level 5: Memory Leaks

// Detect growing memory
setInterval(() => {
  const mem = process.memoryUsage();
  console.log(`Heap: ${(mem.heapUsed / 1024 / 1024).toFixed(0)}MB | RSS: ${(mem.rss / 1024 / 1024).toFixed(0)}MB`);
}, 10_000);

// Take heap snapshots in Chrome DevTools
// Compare snapshot A vs B → see what grew
Enter fullscreen mode Exit fullscreen mode

Common leak patterns:

// ❌ Global variable accumulation
global.cache = global.cache || [];
global.cache.push(heavyObject); // Never cleaned up!

// ❌ Event listener not removed
emitter.on('data', handler); // Never off()
// Fix: emitter.once() when possible, or track and remove

// ❌ Closure holding references
function createHandler() {
  const hugeData = new Array(1000000).fill('x');
  return () => { /* closure keeps hugeData alive */ };
}

// ✅ WeakMap for cache (GC-friendly)
const cache = new WeakMap();
cache.set(obj, heavyData); // Auto-cleaned when obj is GC'd
Enter fullscreen mode Exit fullscreen mode

Production Debugging

// src/middleware/errorLogger.js
module.exports = (err, req, res, _next) => {
  const errorId = Date.now().toString(36) + Math.random().toString(36).slice(2);

  // Log structured error
  const errorLog = {
    id: errorId,
    timestamp: new Date().toISOString(),
    error: err.message,
    stack: err.stack,
    path: req.path,
    method: req.method,
    ip: req.ip,
    userId: req.user?.id,
    body: req.body,
    headers: {
      'user-agent': req.headers['user-agent'],
      'content-type': req.headers['content-type']
    }
  };

  console.error(JSON.stringify(errorLog)); // Structured JSON for log aggregation

  // In production: don't expose internals
  const statusCode = err.statusCode || 500;
  res.status(statusCode).json({
    error: {
      code: err.code || 'INTERNAL_ERROR',
      message: process.env.NODE_ENV === 'production' 
        ? `Internal error (ref: ${errorId})` 
        : err.message,
      ref: errorId
    }
  });
};
Enter fullscreen mode Exit fullscreen mode

My Debugging Checklist

When a bug report comes in:

□ Reproduce locally (same inputs, same env)
□ Check recent changes (git log --since="1 week ago")
□ Read the ACTUAL error message (not what you think it says)
□ Check logs around the timestamp
□ Add ONE strategic debugger/console.log
□ Verify assumptions (is the var really what you think?)
□ Simplify (remove layers until it works, then add back)
□ Check edge cases (empty, null, huge, unicode, concurrent)
□ Ask: "What if the data is different than expected?"
□ After fix: Write a test that would have caught this
Enter fullscreen mode Exit fullscreen mode

What's your go-to debugging trick? Console.log ninja or IDE power user?

Follow @armorbreak for more practical Node.js guides.

Top comments (0)