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:
- Master launch.json configuration for multiple debug scenarios
- Understand the Debug Adapter Protocol (DAP) that powers VS Code debugging
- Configure conditional breakpoints, logpoints, and watch expressions
- Debug TypeScript with source maps for compiled language support
- 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:
- You have to modify code to debug it
- You have to remember to remove the logs later
- You can’t inspect complex objects deeply
- You can’t step through execution
- 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 sessionattach: Connect to a running processsetBreakpoints: Tell the debugger where to pausecontinue,stepOver,stepInto,stepOut: Control executionevaluate: 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
- Complete launch.json: Multiple configurations for different scenarios
- Debugging Demo: A buggy application that you fix using the debugger
- Documentation: Notes on when to use each breakpoint type
- 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.
- Create a test project:
mkdir debug-practice && cd debug-practice
npm init -y
- 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);
});
- 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"
}
]
}
- Set a breakpoint and debug:
- Click in the gutter at line 23 (inside
processPayment) - Press
F5to start debugging - Observe the Variables panel and call stack
- Click in the gutter at line 23 (inside
Phase 2: Conditional Breakpoints and Logpoints (45 minutes)
Goal: Use advanced breakpoint types.
- Set a conditional breakpoint:
- Right-click an existing breakpoint → “Edit Breakpoint”
- Enter condition:
user.isPremium === true - Now the breakpoint only triggers for premium users
- 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)
- Create a loop in your code:
- 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
- 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.
- Add attach configuration to launch.json:
{
"type": "node",
"request": "attach",
"name": "Attach to Process",
"port": 9229,
"restart": true,
"skipFiles": ["<node_internals>/**"]
}
- Start your app with debugging enabled:
node --inspect=9229 app.js
- Attach the debugger:
- Press
F5and select “Attach to Process” - Set breakpoints—they’ll work on the running process
- Press
- Use
--inspect-brkfor 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.
- Create a TypeScript project:
mkdir ts-debug && cd ts-debug
npm init -y
npm install typescript @types/node --save-dev
npx tsc --init
- Configure tsconfig.json for source maps:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true
}
}
- 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));
- 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
}
- Debug:
- Set a breakpoint in
app.ts(the TypeScript file) - Press
F5 - The debugger runs compiled JS but shows you TypeScript!
- Set a breakpoint in
Phase 5: Debug Tests and Compound Configurations (45 minutes)
Goal: Create advanced debugging scenarios.
- 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"
}
- Create compound configuration for full-stack:
{
"compounds": [
{
"name": "Server + Client",
"configurations": ["Launch Server", "Launch Chrome"],
"stopAll": true
}
]
}
- 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
- Find the Premium Discount Bug
- User 1 (Alice) is premium but doesn’t get a 10% discount
- Use conditional breakpoint:
user.isPremium === true - Inspect
finalAmountvs expected value
- Trace Async Flow
- Set breakpoints in
fetchUser,processPayment,checkout - Step through async/await execution
- Watch call stack as promises resolve
- Set breakpoints in
- 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
outFilespattern 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
-
“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.”
-
“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.”
-
“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 setsourceMaps: trueandoutFilesto point to the compiled JS. The debugger shows TypeScript source but runs the JavaScript.” -
“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