Project 2: The "Time Travel" Debugger Configuration

Project 2: The “Time Travel” Debugger Configuration

Project Overview

Attribute Value
Difficulty Intermediate
Time Estimate Weekend
Main Language Node.js / JSON
Alternative Languages Python, C++, Go
Knowledge Area Debugging / Configuration
Prerequisites Node.js installed, basic understanding of async code

Learning Objectives

By completing this project, you will:

  1. Master launch.json configuration for multiple debug scenarios
  2. Understand the Debug Adapter Protocol (DAP) that powers VS Code debugging
  3. Configure conditional breakpoints, logpoints, and watch expressions
  4. Debug TypeScript with source maps for compiled language support
  5. Create compound configurations for full-stack debugging

The Core Question

“How do I inspect what my code is actually doing at runtime, not what I think it’s doing?”


Deep Theoretical Foundation

The Problem with console.log Debugging

Most developers debug with console.log statements. This is slow and pollutes the codebase:

// The console.log approach
async function getUser(userId) {
  console.log('Getting user:', userId);  // Added for debugging
  const response = await fetch(`/api/users/${userId}`);
  console.log('Response:', response);    // Added for debugging
  const data = await response.json();
  console.log('Data:', data);            // Added for debugging
  return data.user;
}

Problems:

  1. You have to modify code to debug it
  2. You have to remember to remove the logs later
  3. You can’t inspect complex objects deeply
  4. You can’t step through execution
  5. You can’t see the call stack

The Debug Adapter Protocol (DAP)

VS Code doesn’t know how to debug Python vs Node.js vs Go. Instead, it uses the Debug Adapter Protocol—a standardized interface between the editor and language-specific debuggers.

┌──────────────┐         DAP          ┌────────────────────┐
│   VS Code    │◄─────────────────────┤  Python Debugger   │
│  (Debugger   │    (JSON-RPC)        │     (debugpy)      │
│     UI)      │                      └────────────────────┘
└──────────────┘                      ┌────────────────────┐
      ▲                               │   Node Debugger    │
      │                               │   (node-inspect)   │
      └───────────────────────────────└────────────────────┘

DAP Messages:

  • launch: Start a new debug session
  • attach: Connect to a running process
  • setBreakpoints: Tell the debugger where to pause
  • continue, stepOver, stepInto, stepOut: Control execution
  • evaluate: Execute expressions in the current scope

The launch.json File

When you press F5, VS Code reads .vscode/launch.json to know how to start debugging:

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",           // Which debug adapter to use
      "request": "launch",      // Start new process (vs "attach")
      "name": "Debug Program",  // Name shown in dropdown
      "program": "${file}",     // What to run
      "env": { "DEBUG": "true" }// Environment variables
    }
  ]
}

Launch vs Attach

Launch Configuration: VS Code starts the process for you with debugging enabled.

  • Use for: Development, testing, new processes

Attach Configuration: You start the process manually, VS Code connects to it.

  • Use for: Production debugging, long-running servers, Docker containers
# Start Node.js with debugging enabled
node --inspect-brk=9229 app.js  # Breaks on first line
node --inspect=9229 app.js       # Runs normally, attach anytime

Breakpoint Types

Regular Breakpoint: Always pauses execution at that line.

Conditional Breakpoint: Only pauses when a condition is true.

// Condition: user.isPremium === true
// Only pauses for premium users, ignores free users

Hit Count Breakpoint: Pauses after N executions.

// Hit count: > 100
// Only pauses after the loop has run 100+ times

Logpoint: Logs a message without pausing (like console.log but no code modification).

// Logpoint message: "User {user.name} processing at {new Date()}"
// Prints to Debug Console, doesn't stop execution

Source Maps: Debugging Compiled Code

TypeScript compiles to JavaScript. When debugging, you want to see TypeScript—not the compiled output.

Source maps are files (.js.map) that map compiled code back to source:

TypeScript (what you see)     JavaScript (what runs)
─────────────────────────     ─────────────────────────
line 42, column 5         ←   line 87, column 12
                          │
                    source map

In launch.json:

{
  "sourceMaps": true,
  "outFiles": ["${workspaceFolder}/dist/**/*.js"]
}

The Call Stack: Time Travel

The call stack shows the sequence of function calls that led to the current line. You can click on any frame to inspect the state at that point.

main()
  └── processOrder()
        └── validatePayment()
              └── checkBalance() ← Current position

Clicking on processOrder shows you the variables as they were when that function was called—before the bug occurred. This is “time travel” debugging.


Project Specification

What You’re Building

A sophisticated debugging setup for a Node.js application with:

  • Launch configuration for normal development
  • Attach configuration for connecting to running processes
  • Test debugging configuration
  • TypeScript source map support
  • Compound configuration for client + server

Deliverables

  1. Complete launch.json: Multiple configurations for different scenarios
  2. Debugging Demo: A buggy application that you fix using the debugger
  3. Documentation: Notes on when to use each breakpoint type
  4. TypeScript Project: A TS project with source map debugging working

Success Criteria

  • Can launch a Node.js app with debugger attached
  • Can attach to a running process on port 9229
  • Can debug TypeScript with source maps
  • Have used conditional breakpoints effectively
  • Have used logpoints without modifying code
  • Have navigated the call stack to find a bug’s origin

Solution Architecture

Debug Configuration Overview

┌─────────────────────────────────────────────────────────────┐
│                    launch.json Structure                     │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  configurations: [                                          │
│    ┌─────────────────────────────────────────────────────┐  │
│    │  "Launch Program"                                   │  │
│    │  - type: "node"                                     │  │
│    │  - request: "launch"                                │  │
│    │  - For: Normal development                          │  │
│    └─────────────────────────────────────────────────────┘  │
│                                                              │
│    ┌─────────────────────────────────────────────────────┐  │
│    │  "Attach to Process"                                │  │
│    │  - type: "node"                                     │  │
│    │  - request: "attach"                                │  │
│    │  - For: Connecting to running servers               │  │
│    └─────────────────────────────────────────────────────┘  │
│                                                              │
│    ┌─────────────────────────────────────────────────────┐  │
│    │  "Debug Tests"                                      │  │
│    │  - type: "node"                                     │  │
│    │  - program: "jest" or "mocha"                       │  │
│    │  - For: Debugging test files                        │  │
│    └─────────────────────────────────────────────────────┘  │
│  ],                                                         │
│                                                              │
│  compounds: [                                               │
│    ┌─────────────────────────────────────────────────────┐  │
│    │  "Server + Client"                                  │  │
│    │  - configurations: ["Backend", "Frontend"]          │  │
│    │  - For: Full-stack debugging                        │  │
│    └─────────────────────────────────────────────────────┘  │
│  ]                                                          │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Variable Reference Guide

Variables available in launch.json:

Variable Value
${workspaceFolder} The opened folder’s root path
${file} Currently open file’s full path
${fileBasename} Current file’s name (e.g., app.js)
${fileDirname} Current file’s directory
${env:HOME} Value of HOME environment variable
${config:editor.fontSize} VS Code setting value

Phased Implementation Guide

Phase 1: Basic Launch Configuration (45 minutes)

Goal: Create a simple debugging setup.

  1. Create a test project:
mkdir debug-practice && cd debug-practice
npm init -y
  1. Create a buggy application (app.js):
// app.js - Buggy application for debugging practice

async function fetchUser(userId) {
  // Simulated API call
  const users = {
    1: { name: 'Alice', isPremium: true, balance: 100 },
    2: { name: 'Bob', isPremium: false, balance: 50 },
    3: { name: 'Charlie', isPremium: true, balance: 0 }
  };

  return users[userId];
}

async function processPayment(user, amount) {
  if (!user) {
    throw new Error('User not found');
  }

  if (user.balance < amount) {
    return { success: false, reason: 'Insufficient funds' };
  }

  // Bug: Should check isPremium for discount
  const finalAmount = amount;  // Should be: user.isPremium ? amount * 0.9 : amount

  user.balance -= finalAmount;
  return { success: true, charged: finalAmount, remaining: user.balance };
}

async function checkout(userId, cartTotal) {
  console.log(`Processing checkout for user ${userId}...`);

  const user = await fetchUser(userId);
  const result = await processPayment(user, cartTotal);

  if (result.success) {
    console.log(`Charged $${result.charged}. Remaining: $${result.remaining}`);
  } else {
    console.log(`Payment failed: ${result.reason}`);
  }

  return result;
}

// Run checkout for premium user (should get 10% discount)
checkout(1, 50).then(result => {
  console.log('Final result:', result);
});
  1. Create launch.json:

Press Cmd+Shift+D to open Debug panel, click “create a launch.json file”, select Node.js:

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Launch Program",
      "skipFiles": ["<node_internals>/**"],
      "program": "${workspaceFolder}/app.js"
    }
  ]
}
  1. Set a breakpoint and debug:
    • Click in the gutter at line 23 (inside processPayment)
    • Press F5 to start debugging
    • Observe the Variables panel and call stack

Phase 2: Conditional Breakpoints and Logpoints (45 minutes)

Goal: Use advanced breakpoint types.

  1. Set a conditional breakpoint:
    • Right-click an existing breakpoint → “Edit Breakpoint”
    • Enter condition: user.isPremium === true
    • Now the breakpoint only triggers for premium users
  2. Set a hit count breakpoint:
    • Create a loop in your code:
      for (let i = 0; i < 1000; i++) {
      processItem(items[i]);
      }
      
    • Right-click breakpoint → Edit → “Hit Count”
    • Enter: > 500 (only pauses after 500 iterations)
  3. Add a logpoint:
    • Right-click in the gutter → “Add Logpoint”
    • Enter: User {user.name} processing amount {amount}
    • Run the debugger—message appears in Debug Console without stopping
  4. Add watch expressions:
    • In Debug panel, find “Watch” section
    • Click + and add: user.balance - amount
    • This expression updates as you step through code

Phase 3: Attach Configuration (30 minutes)

Goal: Debug a running process.

  1. Add attach configuration to launch.json:
{
  "type": "node",
  "request": "attach",
  "name": "Attach to Process",
  "port": 9229,
  "restart": true,
  "skipFiles": ["<node_internals>/**"]
}
  1. Start your app with debugging enabled:
node --inspect=9229 app.js
  1. Attach the debugger:
    • Press F5 and select “Attach to Process”
    • Set breakpoints—they’ll work on the running process
  2. Use --inspect-brk for startup debugging:
node --inspect-brk=9229 app.js

This pauses on the first line, letting you set breakpoints before any code runs.

Phase 4: TypeScript Source Maps (45 minutes)

Goal: Debug TypeScript code.

  1. Create a TypeScript project:
mkdir ts-debug && cd ts-debug
npm init -y
npm install typescript @types/node --save-dev
npx tsc --init
  1. Configure tsconfig.json for source maps:
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "sourceMap": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true
  }
}
  1. Create src/app.ts:
interface User {
  name: string;
  age: number;
}

function greet(user: User): string {
  const greeting = `Hello, ${user.name}! You are ${user.age} years old.`;
  return greeting;
}

const alice: User = { name: 'Alice', age: 30 };
console.log(greet(alice));
  1. Update launch.json for TypeScript:
{
  "type": "node",
  "request": "launch",
  "name": "Debug TypeScript",
  "program": "${workspaceFolder}/src/app.ts",
  "preLaunchTask": "tsc: build - tsconfig.json",
  "outFiles": ["${workspaceFolder}/dist/**/*.js"],
  "sourceMaps": true
}
  1. Debug:
    • Set a breakpoint in app.ts (the TypeScript file)
    • Press F5
    • The debugger runs compiled JS but shows you TypeScript!

Phase 5: Debug Tests and Compound Configurations (45 minutes)

Goal: Create advanced debugging scenarios.

  1. Add Jest test configuration:
{
  "type": "node",
  "request": "launch",
  "name": "Debug Jest Tests",
  "program": "${workspaceFolder}/node_modules/.bin/jest",
  "args": ["--runInBand", "--no-cache", "${file}"],
  "console": "integratedTerminal",
  "internalConsoleOptions": "neverOpen"
}
  1. Create compound configuration for full-stack:
{
  "compounds": [
    {
      "name": "Server + Client",
      "configurations": ["Launch Server", "Launch Chrome"],
      "stopAll": true
    }
  ]
}
  1. Complete launch.json example:
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Launch Program",
      "skipFiles": ["<node_internals>/**"],
      "program": "${workspaceFolder}/app.js",
      "env": {
        "NODE_ENV": "development",
        "DEBUG": "app:*"
      }
    },
    {
      "type": "node",
      "request": "attach",
      "name": "Attach to Process",
      "port": 9229,
      "restart": true,
      "skipFiles": ["<node_internals>/**"]
    },
    {
      "type": "node",
      "request": "launch",
      "name": "Debug TypeScript",
      "program": "${workspaceFolder}/src/app.ts",
      "preLaunchTask": "tsc: build - tsconfig.json",
      "outFiles": ["${workspaceFolder}/dist/**/*.js"],
      "sourceMaps": true
    },
    {
      "type": "node",
      "request": "launch",
      "name": "Debug Jest Tests",
      "program": "${workspaceFolder}/node_modules/.bin/jest",
      "args": ["--runInBand", "--no-cache"],
      "console": "integratedTerminal"
    }
  ],
  "compounds": [
    {
      "name": "Full Stack",
      "configurations": ["Launch Program", "Debug TypeScript"]
    }
  ]
}

Testing Strategy

Debugging Scenarios to Practice

  1. Find the Premium Discount Bug
    • User 1 (Alice) is premium but doesn’t get a 10% discount
    • Use conditional breakpoint: user.isPremium === true
    • Inspect finalAmount vs expected value
  2. Trace Async Flow
    • Set breakpoints in fetchUser, processPayment, checkout
    • Step through async/await execution
    • Watch call stack as promises resolve
  3. Debug a Test Failure
    • Write a failing test
    • Set breakpoint in test file
    • Step into application code to find the bug

Common Pitfalls and Debugging

Pitfall 1: Breakpoints Not Hit

Problem: You set breakpoints but they’re gray or not triggered.

Solutions:

  • Ensure source maps are enabled for TypeScript
  • Check outFiles pattern matches your compiled JS
  • Verify the file path matches exactly

Pitfall 2: “Cannot Find Module” Errors

Problem: Debugger can’t find your entry point.

Solution: Use absolute paths or workspace-relative variables:

"program": "${workspaceFolder}/src/app.js"

Pitfall 3: Attach Fails to Connect

Problem: “Cannot connect to runtime process” when attaching.

Solutions:

  • Ensure process is running with --inspect
  • Check port number matches (default: 9229)
  • Check no firewall blocking the port

Pitfall 4: TypeScript Breakpoints in Wrong Location

Problem: Breakpoint jumps to a different line.

Solution: Clean and rebuild:

rm -rf dist/
npx tsc

Source maps might be stale.


Extensions and Challenges

Extension 1: Remote Debugging

Debug a Node.js app running in Docker:

{
  "type": "node",
  "request": "attach",
  "name": "Docker Debug",
  "port": 9229,
  "localRoot": "${workspaceFolder}",
  "remoteRoot": "/app"
}

Extension 2: Multi-Process Debugging

Debug a microservices setup with multiple processes using compound configurations.

Extension 3: Exception Breakpoints

Configure the debugger to pause on ALL exceptions:

  • Debug panel → Breakpoints section
  • Check “All Exceptions” or “Uncaught Exceptions”

Real-World Connections

Production Debugging

Attach to a production process to debug without restarting:

node --inspect=0.0.0.0:9229 app.js
# Connect from VS Code remotely

Performance Profiling

Use Chrome DevTools with Node.js:

node --inspect app.js
# Open chrome://inspect in Chrome

Debugging CI Failures

Reproduce CI environment locally with environment variables:

"env": {
  "CI": "true",
  "NODE_ENV": "test"
}

Interview Questions

  1. “How do you debug asynchronous code?”

    Answer: “I use breakpoints and step through async/await. VS Code’s debugger handles promises automatically—it pauses at each await. I also use watch expressions to monitor promise states and logpoints to trace execution flow without stopping.”

  2. “Explain launch vs attach configurations.”

    Answer: “Launch starts a new process with the debugger attached. Attach connects to an already-running process. I use launch for development and attach for debugging production builds or servers I don’t want to restart.”

  3. “How do you debug TypeScript in VS Code?”

    Answer: “TypeScript compiles to JavaScript, so I need source maps. In tsconfig.json, I enable sourceMap: true. In launch.json, I set sourceMaps: true and outFiles to point to the compiled JS. The debugger shows TypeScript source but runs the JavaScript.”

  4. “What are conditional breakpoints?”

    Answer: “Conditional breakpoints only pause when a condition is true. I use them for debugging specific scenarios—like pausing only when userId === '12345' in a loop that processes thousands of users. This saves time compared to pausing on every iteration.”


Resources

Essential Reading

Book Author Relevant Chapters
Node.js Debugging in VS Code (Official Docs) Microsoft All sections
Debugging TypeScript (Official Docs) Microsoft Source Maps, Breakpoints
The Art of Debugging Norman Matloff Ch 2 (Debugging Principles)
Effective Debugging Diomidis Spinellis Ch 1 (Strategies), Ch 3 (Debugger Use)

Official Documentation


Self-Assessment Checklist

Before considering this project complete, verify:

  • I can create launch and attach configurations
  • I understand when to use launch vs attach
  • I can debug TypeScript with source maps
  • I have used conditional breakpoints effectively
  • I have used logpoints to trace execution
  • I can navigate the call stack to find bug origins
  • I have a working compound configuration for multi-process debugging
  • I can debug Jest/Mocha tests with breakpoints

Previous: P01-keyboard-warrior-refactoring-kata.md Next: P03-one-touch-automation-task-runner.md