JavaScript and TypeScript course Β· Module 5: Advanced JavaScript

Breakpoints and Step-by-Step Code Execution

10 min read
In this lesson8

In the previous exercise we learned about the browser console and its logging functions. However, even the best console messages can be insufficient when dealing with complex bugs in the Jurassic Park management system. What if the validateDinosaurContainment() function returns an incorrect result but we don't know why? What if the predator behavior tracking algorithm has a subtle bug?

In such cases we need the ability to pause code execution at a specific point and analyze the application state. This is where breakpoints and step-by-step code execution come in - some of the most powerful JavaScript debugging techniques.

What Are Breakpoints?

A breakpoint is a marker placed in code that tells the browser to stop code execution when it reaches that point. This allows detailed examination of the application state at a chosen moment - we can check variable values, inspect the call stack, and execute code step by step.

Setting Breakpoints in the Browser

There are several ways to set breakpoints in the browser:

1. Breakpoints in the Sources/Debugger Panel

The simplest way is to use the Sources panel (Chrome) or Debugger panel (Firefox):

  1. Open developer tools (<kbd>F12</kbd> or <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>I</kbd>)
  2. Go to the "Sources" tab (Chrome) or "Debugger" tab (Firefox)
  3. Find and open the file where you want to set a breakpoint
  4. Click the line number where you want to pause execution
  5. A blue arrow or marker will appear at the selected point, indicating an active breakpoint

For example, we may want to pause execution when checking fence status in Jurassic Park:

1function checkFenceStatus(sectorId) {
2  const sector = getSector(sectorId); // Set breakpoint on this line
3
4  if (!sector) {
5    throw new Error(`Unknown sector: ${sectorId}`);
6  }
7
8  const fenceStatus = sector.fence.getStatus();
9
10  if (fenceStatus !== 'active') {
11    triggerAlarm(sectorId, `Fence failure: ${fenceStatus}`);
12  }
13
14  return fenceStatus;
15}

After setting the breakpoint, when checkFenceStatus() is called, the browser will pause execution at that line and we'll be able to check the value of sectorId and other available variables.

2. Programmatic Breakpoints with the debugger Statement

Sometimes it's more convenient to place a breakpoint directly in the code using the debugger statement:

1function trackDinosaur(dinosaurId, location) {
2  const dinosaur = getDinosaurById(dinosaurId);
3
4  debugger; // Code will pause here when the function is called
5
6  if (!dinosaur) {
7    console.error(`Dinosaur not found with ID: ${dinosaurId}`);
8    return false;
9  }
10
11  updateDinosaurLocation(dinosaurId, location);
12  checkContainmentStatus(dinosaurId, location);
13
14  return true;
15}

The debugger statement is ignored when developer tools are not open, but when active, the browser pauses execution at that point.

3. Conditional Breakpoints

Sometimes we want to pause execution only when a certain condition is met - for example, when tracking a dinosaur with a specific ID or when the danger level exceeds a value:

Setting via Browser Interface

  1. Set a regular breakpoint by clicking the line number
  2. Right-click on the breakpoint
  3. Select "Edit breakpoint" or "Add condition"
  4. Enter a condition, for example: dinosaurId === 'TRX-01' or dangerLevel > 8

Programmatic Conditional Pause

We can also use a conditional construct with the debugger statement:

1function monitorDinosaurBehavior(dinosaur) {
2  const behaviorPattern = analyzeBehavior(dinosaur);
3  const dangerLevel = assessThreatLevel(behaviorPattern);
4
5  // Pause execution only if the danger level is high
6  if (dangerLevel > 8) {
7    debugger; // Will pause only for high danger levels
8
9    // Code to handle dangerous behaviors
10    activateEmergencyProtocol(dinosaur.id, dangerLevel);
11  }
12
13  return {
14    dinosaurId: dinosaur.id,
15    behaviorPattern,
16    dangerLevel,
17    timestamp: new Date()
18  };
19}

Step-by-Step Code Execution

When code is paused at a breakpoint, we have several step execution options that allow controlled traversal of the code:

1. Step Over (F10)

Executes the current line and stops at the next line in the same function. If the current line contains a function call, that function is executed entirely (we don't step inside it).

1function monitorParkSystems() {
2  checkSecuritySystems();     // Step Over executes this entire function...
3  checkDinosaurContainment(); // ...and stops here
4  checkVisitorSafety();
5}

2. Step Into (F11)

Executes the current line, but if the line contains a function call, the debugger "enters" that function and stops at its first line.

1function monitorParkSystems() {
2  checkSecuritySystems();     // Step Into will enter this function...
3  checkDinosaurContainment(); // ...instead of moving here
4  checkVisitorSafety();
5}
6
7function checkSecuritySystems() {
8  const fenceStatus = checkFences();   // ...and will stop here
9  const cameraStatus = checkCameras();
10  const gateStatus = checkGates();
11
12  return { fenceStatus, cameraStatus, gateStatus };
13}

3. Step Out (Shift+F11)

Executes the remaining part of the current function and stops after returning from the function, at the point where it was called.

1function checkSecuritySystems() {
2  const fenceStatus = checkFences();
3  const cameraStatus = checkCameras(); // If we are here...
4  const gateStatus = checkGates();
5
6  return { fenceStatus, cameraStatus, gateStatus };
7}
8
9function monitorParkSystems() {
10  checkSecuritySystems();     // ...Step Out executes the rest of the function and stops here
11  checkDinosaurContainment();
12  checkVisitorSafety();
13}

4. Continue (F8)

Resumes code execution until the next breakpoint is encountered or the script finishes.

Inspecting Variables and State

When code is paused at a breakpoint, we can thoroughly examine the application state:

1. Scope/Variables Panel

One of the most important panels during debugging - shows all available variables in the current scope:

  • Local - local variables in the current function
  • Closure - variables from closures (parent functions)
  • Global - global variables
  • this - the value of this in the current context

For example, when debugging a function monitoring dinosaur behavior:

1function analyzeDinosaurBehavior(dinosaur, environmentalFactors) {
2  const { species, previousBehaviors, lastFeeding } = dinosaur;
3  const { weather, timeOfDay, nearbyDinosaurs } = environmentalFactors;
4
5  const timeSinceFeeding = Date.now() - lastFeeding;
6  const isHungry = timeSinceFeeding > getSpeciesFeedingThreshold(species);
7
8  debugger; // pause here
9
10  // At this point the Scope panel will show:
11  // - Local: dinosaur, environmentalFactors, species, previousBehaviors, lastFeeding,
12  //          weather, timeOfDay, nearbyDinosaurs, timeSinceFeeding, isHungry
13  // - Global: global objects and functions
14
15  const behaviorPrediction = predictBehavior(
16    species, previousBehaviors, isHungry, weather, timeOfDay, nearbyDinosaurs
17  );
18
19  return {
20    dinosaurId: dinosaur.id,
21    prediction: behaviorPrediction,
22    riskLevel: assessRiskLevel(behaviorPrediction),
23    isHungry,
24    timestamp: new Date()
25  };
26}

2. Watch Expressions

The Watch panel lets you add expressions to monitor during debugging. This is especially useful for complex expressions or values not directly visible in the Scope panel:

1// We can add watched expressions such as:
2dinosaur.species === 'Velociraptor'
3getDistanceFromFence(dinosaur.location)
4isInDangerZone(dinosaur.location, restrictedSectors)

3. Console in Debug Mode

While paused at a breakpoint, the console operates in the context of the current code position. We can execute expressions that have access to all variables visible in the current scope:

1// In the console we can type:
2dinosaur.species
3timeSinceFeeding / (1000 * 60 * 60) // Convert milliseconds to hours
4nearbyDinosaurs.filter(d => d.species === 'Velociraptor')

This is a powerful way to interactively explore the application state and run experiments without modifying code.

Practical Debugging Techniques for Jurassic Park

1. Debugging Asynchronous Code

Jurassic Park systems often use asynchronous operations - fetching data from sensors, communicating with security systems, etc. Debugging such code requires special techniques:

1async function monitorsEnvironmentalSensors() {
2  try {
3    console.log("Starting sensor monitoring...");
4
5    // Asynchronously fetch sensor data
6    const temperatureData = await fetchSensorData('temperature');
7    debugger; // Breakpoint pauses after receiving the response
8
9    const humidityData = await fetchSensorData('humidity');
10    const windData = await fetchSensorData('wind');
11
12    const environmentalRisk = analyzeEnvironmentalRisk(
13      temperatureData, humidityData, windData
14    );
15
16    if (environmentalRisk.level === 'high') {
17      notifyParkManagement(`High environmental risk detected: ${environmentalRisk.reason}`);
18    }
19
20    return environmentalRisk;
21  } catch (error) {
22    console.error("Sensor monitoring error:", error);
23    triggerBackupSystems();
24    return { level: 'unknown', reason: 'error' };
25  }
26}

In modern browser developer tools, breakpoints work correctly with async functions - execution will pause at the debugger point, even after await operations.

2. Pausing on Exceptions

In a complex system like Jurassic Park, errors can appear in unexpected places. Instead of manually setting breakpoints, we can configure the debugger to pause when an exception occurs:

  1. Open the Sources/Debugger panel
  2. Find the "Breakpoints" section
  3. Check "Pause on exceptions"
  4. Optionally: check "Pause on caught exceptions"

Now the browser will pause execution when any unhandled exception occurs, greatly simplifying identification of the source of problems.

3. Source Maps

In production applications, JavaScript code is often minified and bundled, making debugging difficult. Source maps allow browsers to show and debug the original source code, even when minified code is being executed:

1// webpack.config.js
2module.exports = {
3  devtool: 'source-map', // Generate source maps for all files
4};

For Jurassic Park, where safety is a priority, this is a critical feature that enables fast debugging of production issues.

Debugging in Practice - Case Study

Let's analyze a practical example of debugging a problem in the dinosaur monitoring system:

1class DinosaurMonitoringSystem {
2  constructor() {
3    this.activeMonitors = new Map();
4    this.alertLevel = 'normal';
5  }
6
7  checkDinosaurStatus(dinosaurId) {
8    const monitor = this.activeMonitors.get(dinosaurId);
9
10    if (!monitor) {
11      console.error(`No active monitoring for dinosaur ${dinosaurId}`);
12      return null;
13    }
14
15    try {
16      const dinosaur = getDinosaurById(dinosaurId);
17      const location = getLastKnownLocation(dinosaurId);
18      const vitalSigns = getVitalSigns(dinosaurId);
19
20      // BUG: Not checking if location exists!
21      const inContainment = isInContainmentZone(location.coordinates);
22
23      const reading = {
24        timestamp: Date.now(),
25        location,
26        vitalSigns,
27        inContainment
28      };
29
30      monitor.readings.push(reading);
31      monitor.locationStatus = inContainment ? 'contained' : 'breach';
32
33      return reading;
34    } catch (error) {
35      console.error(`Error checking status for dinosaur ${dinosaurId}:`, error);
36      return null;
37    }
38  }
39}

Step 1: Identify the Problem

During testing, the monitoring system unexpectedly reports 'critical' alert level for some dinosaurs, even though they are safely in their enclosures.

Step 2: Set Breakpoints

We set a breakpoint in checkDinosaurStatus to investigate:

1checkDinosaurStatus(dinosaurId) {
2  const monitor = this.activeMonitors.get(dinosaurId);
3  if (!monitor) { return null; }
4
5  debugger; // Set breakpoint here to examine all variables
6
7  try { /* ...rest of code */ }
8}

Step 3: Step Through and Analyze

  1. Step through code using "Step Into" and "Step Over"
  2. We reach the line checking location status:
    1const inContainment = isInContainmentZone(location.coordinates);
  3. Check the location variable in the Scope panel - it's null
  4. Identified problem: getLastKnownLocation sometimes returns null when it can't find the location, and the code doesn't check for this before accessing location.coordinates

Step 4: Fix the Bug

1// Safer code with verification
2const location = getLastKnownLocation(dinosaurId);
3const inContainment = location && location.coordinates
4  ? isInContainmentZone(location.coordinates)
5  : false; // If we don't know the location, assume the worst

Step 5: Verify the Solution

After fixing the bug, remove the breakpoint and test the system again. This time it works correctly without reporting false alarms.

Best Practices for Debugging

  1. Use meaningful variable names - makes identifying variables during debugging easier
  2. Add documentation to functions - JSDoc comments help understand the purpose and expected values
  3. Logically structure code - clean code is easier to debug
  4. Use defensive programming - validate inputs and handle edge cases
  5. Create small, dedicated functions - functions with single responsibility are easier to debug
  6. Use try-catch for potentially problematic code - helps localize error sources
  7. Avoid modifying debugged variables - it can change program behavior

Summary

Breakpoints and step-by-step code execution are invaluable tools in the arsenal of every developer working on Jurassic Park systems. They allow:

  • Pausing code execution at critical moments
  • Examining application state, variable values, and execution context
  • Executing code step by step to understand its flow
  • Identifying error sources, especially in complex systems
  • Verifying correctness of functions and modules

Remember that in Jurassic Park, the ability to quickly find and fix bugs can be a matter of life or death - both for the code and (in a real park) for the visitors!

Code for this lesson: main.js
1// Circular Dependencies - Jurassic Park
2// Handling circular dependencies in modules
3
4console.log("Circular Dependencies");
5console.log("How to handle mutual module dependencies\n");
6
7// ===========================================
8// PROBLEM: Circular Dependencies
9// ===========================================
10
11// === dinosaur-manager.js ===
12class DinosaurManager {
13  constructor() {
14    this.dinosaurs = new Map();
15  }
16
17  addDinosaur(dinosaur) {
18    this.dinosaurs.set(dinosaur.id, dinosaur);
19    // Problem: we need SecuritySystem, but it also needs DinosaurManager
20    console.log(`Added dinosaur: ${dinosaur.name}`);
21  }
22
23  getDinosaur(id) {
24    return this.dinosaurs.get(id);
25  }
26
27  getAllDinosaurs() {
28    return Array.from(this.dinosaurs.values());
29  }
30}
31
32// === security-system.js ===
33class SecuritySystem {
34  constructor(dinosaurManager) {
35    this.dinosaurManager = dinosaurManager;
36    this.securityLevel = 'GREEN';
37  }
38
39  checkSecurity() {
40    const dinosaurs = this.dinosaurManager.getAllDinosaurs();
41    const dangerousDinos = dinosaurs.filter(d => d.dangerLevel > 7);
42
43    if (dangerousDinos.length > 2) {
44      this.securityLevel = 'RED';
45    } else if (dangerousDinos.length > 0) {
46      this.securityLevel = 'YELLOW';
47    } else {
48      this.securityLevel = 'GREEN';
49    }
50
51    console.log(`Security level: ${this.securityLevel}`);
52    return this.securityLevel;
53  }
54
55  alertForDinosaur(dinosaur) {
56    if (dinosaur.dangerLevel > 7) {
57      console.log(`ALERT: ${dinosaur.name} is dangerous!`);
58    }
59  }
60}
61
62// === dinosaur.js ===
63class Dinosaur {
64  constructor(name, species, dangerLevel, securitySystem) {
65    this.id = Math.random().toString(36).substr(2, 9);
66    this.name = name;
67    this.species = species;
68    this.dangerLevel = dangerLevel;
69    this.securitySystem = securitySystem;
70  }
71
72  roar() {
73    console.log(`${this.name}: ROAAR!`);
74    // Problem: we need SecuritySystem, which needs DinosaurManager
75    if (this.securitySystem) {
76      this.securitySystem.alertForDinosaur(this);
77    }
78  }
79}
80
81// ===========================================
82// SOLUTION 1: Dependency Injection
83// ===========================================
84
85console.log("=== SOLUTION 1: Dependency Injection ===\n");
86
87const manager = new DinosaurManager();
88const security = new SecuritySystem(manager);
89
90const trex = new Dinosaur('Rexy', 'T-Rex', 9, security);
91const raptor = new Dinosaur('Blue', 'Velociraptor', 7, security);
92const brachi = new Dinosaur('Gentle Giant', 'Brachiosaurus', 2, security);
93
94manager.addDinosaur(trex);
95manager.addDinosaur(raptor);
96manager.addDinosaur(brachi);
97
98console.log("\nChecking security...");
99security.checkSecurity();
100
101console.log("\nSound test:");
102trex.roar();
103raptor.roar();
104brachi.roar();
105
106// ===========================================
107// SOLUTION 2: Event System
108// ===========================================
109
110console.log("\n=== SOLUTION 2: Event System ===\n");
111
112class EventBus {
113  constructor() {
114    this.listeners = {};
115  }
116
117  on(event, callback) {
118    if (!this.listeners[event]) {
119      this.listeners[event] = [];
120    }
121    this.listeners[event].push(callback);
122  }
123
124  emit(event, data) {
125    if (this.listeners[event]) {
126      this.listeners[event].forEach(callback => callback(data));
127    }
128  }
129}
130
131const eventBus = new EventBus();
132
133class DinosaurManagerV2 {
134  constructor(eventBus) {
135    this.eventBus = eventBus;
136    this.dinosaurs = new Map();
137
138    // Listen for events
139    this.eventBus.on('dinosaur:added', (dino) => {
140      console.log(`Event: Added ${dino.name}`);
141    });
142  }
143
144  addDinosaur(dinosaur) {
145    this.dinosaurs.set(dinosaur.id, dinosaur);
146    this.eventBus.emit('dinosaur:added', dinosaur);
147  }
148
149  getAllDinosaurs() {
150    return Array.from(this.dinosaurs.values());
151  }
152}
153
154class SecuritySystemV2 {
155  constructor(eventBus) {
156    this.eventBus = eventBus;
157    this.securityLevel = 'GREEN';
158
159    // Listen for events
160    this.eventBus.on('dinosaur:added', (dino) => {
161      this.evaluateThreat(dino);
162    });
163  }
164
165  evaluateThreat(dinosaur) {
166    if (dinosaur.dangerLevel > 7) {
167      console.log(`Event Security: ${dinosaur.name} requires monitoring`);
168      this.securityLevel = 'YELLOW';
169    }
170  }
171}
172
173const eventBusInstance = new EventBus();
174const managerV2 = new DinosaurManagerV2(eventBusInstance);
175const securityV2 = new SecuritySystemV2(eventBusInstance);
176
177console.log("Adding dinosaurs with the event system:");
178managerV2.addDinosaur({ id: '1', name: 'Rex Jr', dangerLevel: 8 });
179managerV2.addDinosaur({ id: '2', name: 'Herbivore', dangerLevel: 2 });
180
181// ===========================================
182// SOLUTION 3: Lazy Loading / Late Binding
183// ===========================================
184
185console.log("\n=== SOLUTION 3: Lazy Loading ===\n");
186
187class DinosaurManagerV3 {
188  constructor() {
189    this.dinosaurs = new Map();
190    this._securitySystem = null;
191  }
192
193  setSecuritySystem(security) {
194    this._securitySystem = security;
195  }
196
197  addDinosaur(dinosaur) {
198    this.dinosaurs.set(dinosaur.id, dinosaur);
199    console.log(`Lazy: Added ${dinosaur.name}`);
200
201    // Lazy evaluation - security is available only now
202    if (this._securitySystem) {
203      this._securitySystem.checkSecurity();
204    }
205  }
206
207  getAllDinosaurs() {
208    return Array.from(this.dinosaurs.values());
209  }
210}
211
212class SecuritySystemV3 {
213  constructor() {
214    this._dinosaurManager = null;
215  }
216
217  setDinosaurManager(manager) {
218    this._dinosaurManager = manager;
219  }
220
221  checkSecurity() {
222    if (!this._dinosaurManager) {
223      console.log("DinosaurManager has not been assigned yet");
224      return;
225    }
226
227    const dinosaurs = this._dinosaurManager.getAllDinosaurs();
228    console.log(`Checking security for ${dinosaurs.length} dinosaurs`);
229  }
230}
231
232const managerV3 = new DinosaurManagerV3();
233const securityV3 = new SecuritySystemV3();
234
235// Set references after creating both objects
236managerV3.setSecuritySystem(securityV3);
237securityV3.setDinosaurManager(managerV3);
238
239managerV3.addDinosaur({ id: '1', name: 'Test Dino', dangerLevel: 5 });
240
241console.log("\n=== BEST PRACTICES ===\n");
242console.log("Dependency Injection:");
243console.log("   - Pass dependencies through constructor/parameters");
244console.log("   - Testable and predictable\n");
245
246console.log("Event System:");
247console.log("   - Loose coupling between modules");
248console.log("   - Scalable for many subscribers\n");
249
250console.log("Lazy Loading:");
251console.log("   - Delayed dependency binding");
252console.log("   - Good for optional dependencies\n");
253
254console.log("AVOID:");
255console.log("   - Direct require/import creating cycles");
256console.log("   - Global variables as solutions");
257console.log("   - Tight coupling between modules\n");
258
259console.log("Demo completed!");

Spotted a mistake in this lesson?

Hands-on tasks in the game

  • Vertical ordering

    Order the code debugging stages from start to finish:

Useful articles