JavaScript and TypeScript course Β· Module 5: Advanced JavaScript
Breakpoints and Step-by-Step Code Execution
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):
- Open developer tools (<kbd>F12</kbd> or <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>I</kbd>)
- Go to the "Sources" tab (Chrome) or "Debugger" tab (Firefox)
- Find and open the file where you want to set a breakpoint
- Click the line number where you want to pause execution
- 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
- Set a regular breakpoint by clicking the line number
- Right-click on the breakpoint
- Select "Edit breakpoint" or "Add condition"
- Enter a condition, for example:
dinosaurId === 'TRX-01'ordangerLevel > 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
thisin 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:
- Open the Sources/Debugger panel
- Find the "Breakpoints" section
- Check "Pause on exceptions"
- 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
- Step through code using "Step Into" and "Step Over"
- We reach the line checking location status:
1const inContainment = isInContainmentZone(location.coordinates); - Check the
locationvariable in the Scope panel - it'snull - Identified problem:
getLastKnownLocationsometimes returnsnullwhen it can't find the location, and the code doesn't check for this before accessinglocation.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 worstStep 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
- Use meaningful variable names - makes identifying variables during debugging easier
- Add documentation to functions - JSDoc comments help understand the purpose and expected values
- Logically structure code - clean code is easier to debug
- Use defensive programming - validate inputs and handle edge cases
- Create small, dedicated functions - functions with single responsibility are easier to debug
- Use try-catch for potentially problematic code - helps localize error sources
- 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: