Debugging & Profiling
Debugging & Profiling¶
Topic: Programming Lesson: 11 of 16 Prerequisites: Error Handling, Testing Fundamentals, Program Flow Control Objective: Develop systematic debugging skills, master debugging tools across languages, understand profiling techniques, and learn to identify and fix performance bottlenecks.
Introduction¶
Debugging is an essential programming skill. Every developer spends significant time debugging ā finding and fixing bugs is often harder than writing code in the first place. The difference between novice and expert developers isn't how many bugs they create (everyone creates bugs), but how efficiently they find and fix them.
This lesson teaches you systematic debugging techniques, introduces debugging tools across multiple languages, covers performance profiling, and helps you develop a debugging mindset that will serve you throughout your career.
The Debugging Mindset¶
Systematic Approach, Not Random Changes¶
Bad debugging:
# Something's wrong... let me just try changing things randomly
result = calculate(x, y) # Doesn't work
result = calculate(y, x) # Try swapping parameters
result = calculate(x + 1, y) # Try adding 1
result = calculate(x, y) * 2 # Try multiplying by 2
# Eventually, something might work, but you don't understand WHY
Good debugging:
1. Observe the bug: What exactly is happening?
2. Form a hypothesis: Why might this be happening?
3. Test the hypothesis: Add logging, use debugger, write a test
4. If hypothesis is wrong, form a new one
5. If hypothesis is right, fix it
6. Verify the fix with tests
The Scientific Method of Debugging¶
Debugging is like scientific investigation:
1. Observe the bug (Reproduce it!)
Before you can fix a bug, you must be able to reproduce it reliably.
Bug report: "The app crashes sometimes when I click the button"
Questions to ask:
- Which button?
- What were you doing before clicking?
- Does it happen every time or randomly?
- What browser/OS/version?
- What error message appears?
Minimal reproducible example:
# Complex scenario (hard to debug)
# "After logging in as admin, navigating to the dashboard,
# clicking reports, filtering by date, and clicking export,
# the app crashes"
# Minimal reproduction (easier to debug)
# "Calling export_report(start_date=None) crashes"
def export_report(start_date):
# Crashes here because start_date is None
return start_date.strftime("%Y-%m-%d") # AttributeError
2. Form a hypothesis
Based on symptoms, what could be causing this?
Symptom: User login fails with "Invalid password" for correct password
Hypotheses:
1. Password is case-sensitive and user is entering wrong case
2. Password hashing algorithm changed
3. Database contains old password hash
4. Whitespace in password field
3. Test the hypothesis
# Hypothesis: Whitespace in password field
# Test: Log the password length and content
def login(username, password):
print(f"Password length: {len(password)}")
print(f"Password repr: {repr(password)}") # Shows whitespace
# ...rest of login logic
4. Fix and verify
# Fix: Strip whitespace
def login(username, password):
password = password.strip()
# ...rest of login logic
# Verify: Write a test
def test_login_with_whitespace():
assert login("alice", " secret123 ") == True
Reproducing Bugs¶
The most critical step in debugging is reproducing the bug reliably.
Recording Steps to Reproduce¶
Steps to reproduce:
1. Navigate to http://localhost:3000/login
2. Enter username: "alice"
3. Enter password: "secret123"
4. Click "Login" button
5. Error appears: "Invalid password"
Expected: User should be logged in
Actual: "Invalid password" error
Minimal Reproducible Example¶
Strip away everything unnecessary:
# Original code (too complex to debug)
def process_user_data(users):
results = []
for user in users:
profile = fetch_profile(user.id)
settings = load_settings(profile)
preferences = parse_preferences(settings)
results.append(format_output(preferences))
return results
# Bug: Sometimes returns empty list
# Minimal reproduction:
def test_bug():
users = [User(id=1), User(id=2)]
results = process_user_data(users)
assert len(results) == 2 # Fails! Returns empty list
# Now debug why the loop doesn't produce results
Print/Log Debugging¶
The simplest debugging technique: add print statements to see what's happening.
Strategic Print Statements¶
def calculate_discount(price, customer_tier, promo_code):
print(f"[DEBUG] Input: price={price}, tier={customer_tier}, code={promo_code}")
base_discount = 0
if customer_tier == "gold":
base_discount = 0.2
print(f"[DEBUG] Gold tier: base_discount={base_discount}")
elif customer_tier == "silver":
base_discount = 0.1
print(f"[DEBUG] Silver tier: base_discount={base_discount}")
promo_discount = 0
if promo_code == "SAVE20":
promo_discount = 0.2
print(f"[DEBUG] Promo code applied: promo_discount={promo_discount}")
total_discount = base_discount + promo_discount
print(f"[DEBUG] Total discount: {total_discount}")
final_price = price * (1 - total_discount)
print(f"[DEBUG] Final price: {final_price}")
return final_price
Output:
[DEBUG] Input: price=100, tier=gold, code=SAVE20
[DEBUG] Gold tier: base_discount=0.2
[DEBUG] Promo code applied: promo_discount=0.2
[DEBUG] Total discount: 0.4
[DEBUG] Final price: 60.0
Structured Logging vs Print¶
Print statements are quick but limited. Use logging for production code:
import logging
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
def process_order(order_id):
logger.info(f"Processing order {order_id}")
try:
order = fetch_order(order_id)
logger.debug(f"Order details: {order}")
validate_order(order)
logger.info(f"Order {order_id} validated successfully")
process_payment(order)
logger.info(f"Payment processed for order {order_id}")
except ValidationError as e:
logger.warning(f"Order {order_id} validation failed: {e}")
raise
except PaymentError as e:
logger.error(f"Payment failed for order {order_id}: {e}")
raise
Output:
2024-01-15 10:30:15,123 - __main__ - INFO - Processing order 12345
2024-01-15 10:30:15,156 - __main__ - DEBUG - Order details: Order(id=12345, total=99.99)
2024-01-15 10:30:15,200 - __main__ - INFO - Order 12345 validated successfully
2024-01-15 10:30:15,450 - __main__ - INFO - Payment processed for order 12345
Interactive Debuggers¶
Debuggers let you pause execution, inspect variables, and step through code line by line.
Common Debugger Operations¶
- Breakpoint: Pause execution at a specific line
- Step Over: Execute current line, move to next line
- Step Into: Enter function calls to debug them
- Step Out: Finish current function, return to caller
- Continue: Resume execution until next breakpoint
- Watch/Inspect: View variable values
- Conditional Breakpoint: Pause only when condition is true
GDB (C/C++)¶
// debug_example.c
#include <stdio.h>
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
int main() {
int result = factorial(5);
printf("Result: %d\n", result);
return 0;
}
Compile with debug symbols:
gcc -g debug_example.c -o debug_example
Debug with GDB:
$ gdb debug_example
(gdb) break factorial # Set breakpoint at factorial function
(gdb) run # Start execution
(gdb) print n # Print value of n
(gdb) next # Execute next line (step over)
(gdb) step # Step into function call
(gdb) continue # Continue to next breakpoint
(gdb) backtrace # Show call stack
(gdb) quit
pdb (Python)¶
# debug_example.py
import pdb
def calculate_average(numbers):
pdb.set_trace() # Debugger will pause here
total = sum(numbers)
count = len(numbers)
average = total / count
return average
numbers = [10, 20, 30, 40, 50]
result = calculate_average(numbers)
print(f"Average: {result}")
Run and debug:
$ python debug_example.py
> debug_example.py(5)calculate_average()
-> total = sum(numbers)
(Pdb) p numbers # Print numbers
[10, 20, 30, 40, 50]
(Pdb) n # Next line
> debug_example.py(6)calculate_average()
-> count = len(numbers)
(Pdb) p total
150
(Pdb) n
> debug_example.py(7)calculate_average()
-> average = total / count
(Pdb) p count
5
(Pdb) c # Continue
Average: 30.0
Modern alternative: breakpoint() (Python 3.7+)
def calculate_average(numbers):
breakpoint() # Automatically invokes debugger
total = sum(numbers)
count = len(numbers)
average = total / count
return average
Chrome DevTools (JavaScript)¶
HTML:
<!DOCTYPE html>
<html>
<body>
<button id="calculate">Calculate</button>
<div id="result"></div>
<script src="app.js"></script>
</body>
</html>
JavaScript:
// app.js
function calculateFactorial(n) {
debugger; // Debugger will pause here when DevTools is open
if (n <= 1) return 1;
return n * calculateFactorial(n - 1);
}
document.getElementById('calculate').addEventListener('click', () => {
const result = calculateFactorial(5);
document.getElementById('result').textContent = `Result: ${result}`;
});
How to debug:
1. Open Chrome DevTools (F12)
2. Go to "Sources" tab
3. Open app.js
4. Click line number to set breakpoint (or use debugger; statement)
5. Trigger the code (click button)
6. Debugger pauses at breakpoint
7. Inspect variables in "Scope" panel
8. Use controls to step through code
IDE Debuggers¶
Most IDEs (Visual Studio Code, PyCharm, IntelliJ IDEA) have built-in visual debuggers: - Click in margin to set breakpoints - Press F5 to start debugging - See variables in side panel - Step through code with toolbar buttons
Visual Studio Code (launch.json):
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: Current File",
"type": "python",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal"
}
]
}
Common Bug Patterns¶
Recognizing these patterns saves hours of debugging:
1. Off-by-One Errors¶
# BUG: Misses last element
def print_array(arr):
for i in range(len(arr) - 1): # Should be len(arr)
print(arr[i])
# BUG: Array index out of bounds
def get_last_element(arr):
return arr[len(arr)] # Should be len(arr) - 1
# BUG: Fence post problem
def count_numbers(start, end):
# Count numbers from start to end inclusive
return end - start # Should be end - start + 1
# If start=1, end=5, there are 5 numbers (1,2,3,4,5), not 4
2. Null/Undefined Reference¶
// BUG: user might be null
function getUserEmail(userId) {
const user = findUser(userId);
return user.email; // TypeError if user is null
}
// FIX: Check for null
function getUserEmail(userId) {
const user = findUser(userId);
if (!user) {
return null;
}
return user.email;
}
// FIX: Optional chaining (ES2020)
function getUserEmail(userId) {
const user = findUser(userId);
return user?.email; // Returns undefined if user is null
}
3. Race Conditions¶
// BUG: Race condition
let counter = 0;
async function incrementCounter() {
const current = counter;
await delay(10); // Simulate async operation
counter = current + 1;
}
// If two calls run concurrently:
// Call 1: reads counter=0
// Call 2: reads counter=0
// Call 1: sets counter=1
// Call 2: sets counter=1
// Expected: 2, Actual: 1
// FIX: Use atomic operations or locks
4. Integer Overflow¶
// BUG: Integer overflow
int a = 2000000000;
int b = 2000000000;
int sum = a + b; // Overflow! Wraps to negative number
System.out.println(sum); // -294967296
// FIX: Use long
long a = 2000000000;
long b = 2000000000;
long sum = a + b; // 4000000000
5. Wrong Operator¶
# BUG: Assignment instead of comparison
if x = 5: # SyntaxError in Python (good!)
print("x is 5")
# In C/C++/Java, this compiles but is wrong:
# if (x = 5) { ... } // Assigns 5 to x, then checks if 5 is truthy (always true)
// BUG: == vs ===
console.log(0 == '0'); // true (type coercion)
console.log(0 === '0'); // false (strict equality)
// Always use === in JavaScript
if (userId === 0) { ... } // Correct
if (userId == 0) { ... } // Dangerous (matches 0, '0', false, '', ...)
6. Scope Issues (Closures)¶
// BUG: All buttons alert "5"
for (var i = 0; i < 5; i++) {
document.getElementById(`button${i}`).addEventListener('click', function() {
alert(i); // All closures reference the same 'i', which is 5 after loop
});
}
// FIX 1: Use let (block scope)
for (let i = 0; i < 5; i++) {
document.getElementById(`button${i}`).addEventListener('click', function() {
alert(i); // Each closure gets its own 'i'
});
}
// FIX 2: IIFE (Immediately Invoked Function Expression)
for (var i = 0; i < 5; i++) {
(function(i) {
document.getElementById(`button${i}`).addEventListener('click', function() {
alert(i);
});
})(i);
}
Advanced Debugging Techniques¶
Rubber Duck Debugging¶
Explain your code to a rubber duck (or any inanimate object). Often, the act of explaining forces you to think clearly and spot the bug.
Developer: "So this function takes a list of numbers and returns the average.
First, I sum the numbers... oh wait, if the list is empty,
I'll divide by zero! That's the bug!"
Binary Search Debugging¶
When you know a bug was introduced between two points in time, use binary search to find when:
Commit A (working) āāāāāāāāāāāāāāāāāāāāāāāŗ Commit Z (broken)
ā²
Test middle commit
Is it working or broken?
Keep dividing the range until you find the exact commit that introduced the bug.
Git Bisect¶
Automate binary search debugging:
# Start bisect
git bisect start
# Mark current commit as bad
git bisect bad
# Mark a known good commit
git bisect good v1.2.0
# Git checks out middle commit
# Test it, then mark as good or bad
git bisect good # or git bisect bad
# Repeat until bug is found
# Git will identify the exact commit
# End bisect
git bisect reset
Divide and Conquer¶
Isolate the problem by commenting out code sections:
def complex_function(data):
# Step 1
processed = preprocess(data)
print(f"After preprocess: {processed}") # Check if this is correct
# Step 2
# transformed = transform(processed)
# print(f"After transform: {transformed}")
# Step 3
# result = aggregate(transformed)
# print(f"After aggregate: {result}")
# return result
return processed # Temporarily return early
# If preprocess output is wrong, debug preprocess()
# If preprocess output is correct, uncomment transform() and debug that
Memory Debugging¶
Memory Leaks: Symptoms and Detection¶
Symptoms: - Application memory usage grows over time - Eventually runs out of memory or crashes - Performance degrades over time
Common causes: - Objects not being freed/garbage collected - Event listeners not removed - File handles not closed - Circular references (in languages without GC)
Valgrind (C/C++)¶
Detect memory leaks and invalid memory access:
// leak.c
#include <stdlib.h>
int main() {
int *arr = malloc(100 * sizeof(int));
// Bug: forgot to free
return 0;
}
Run Valgrind:
$ gcc -g leak.c -o leak
$ valgrind --leak-check=full ./leak
==12345== HEAP SUMMARY:
==12345== in use at exit: 400 bytes in 1 blocks
==12345== total heap usage: 1 allocs, 0 frees, 400 bytes allocated
==12345==
==12345== 400 bytes in 1 blocks are definitely lost
==12345== at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==12345== by 0x40053E: main (leak.c:4)
Fix:
int main() {
int *arr = malloc(100 * sizeof(int));
free(arr); // Free allocated memory
return 0;
}
memory_profiler (Python)¶
# memory_test.py
from memory_profiler import profile
@profile
def create_large_list():
large_list = [i for i in range(1000000)]
return large_list
@profile
def main():
result = create_large_list()
del result # Explicitly delete to free memory
if __name__ == "__main__":
main()
Run:
$ python -m memory_profiler memory_test.py
Line # Mem usage Increment Line Contents
================================================
4 38.7 MiB 38.7 MiB @profile
5 def create_large_list():
6 76.3 MiB 37.6 MiB large_list = [i for i in range(1000000)]
7 76.3 MiB 0.0 MiB return large_list
Chrome DevTools Memory Profiler (JavaScript)¶
- Open Chrome DevTools ā Memory tab
- Take heap snapshot
- Perform actions in your app
- Take another heap snapshot
- Compare snapshots to see what objects were created and not freed
Common leak pattern:
// BUG: Event listener leak
class Component {
constructor() {
this.handleClick = this.handleClick.bind(this);
window.addEventListener('click', this.handleClick);
}
handleClick() {
console.log('Clicked');
}
// Bug: No cleanup when component is destroyed
}
// FIX: Remove listener in cleanup
class Component {
constructor() {
this.handleClick = this.handleClick.bind(this);
window.addEventListener('click', this.handleClick);
}
handleClick() {
console.log('Clicked');
}
destroy() {
window.removeEventListener('click', this.handleClick);
}
}
Performance Profiling¶
Profiling identifies performance bottlenecks: which functions are slow, which are called most often.
CPU Profiling: Hotspots and Call Graphs¶
Hotspot: A function that consumes a lot of CPU time.
cProfile (Python)¶
# slow_program.py
def is_prime(n):
if n < 2:
return False
for i in range(2, n):
if n % i == 0:
return False
return True
def find_primes(max_num):
primes = []
for i in range(2, max_num):
if is_prime(i):
primes.append(i)
return primes
if __name__ == "__main__":
result = find_primes(10000)
print(f"Found {len(result)} primes")
Profile it:
$ python -m cProfile -s cumulative slow_program.py
54235 function calls in 2.841 seconds
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 2.841 2.841 slow_program.py:1(<module>)
1 0.010 0.010 2.841 2.841 slow_program.py:9(find_primes)
9998 2.831 0.000 2.831 0.000 slow_program.py:1(is_prime)
Optimization:
def is_prime(n):
if n < 2:
return False
if n == 2:
return True
if n % 2 == 0:
return False
# Only check odd numbers up to sqrt(n)
for i in range(3, int(n**0.5) + 1, 2):
if n % i == 0:
return False
return True
After optimization:
$ python -m cProfile -s cumulative slow_program.py
10235 function calls in 0.051 seconds
# 55x faster!
Chrome DevTools Performance Tab (JavaScript)¶
- Open DevTools ā Performance tab
- Click Record
- Perform the slow operation
- Stop recording
- Analyze the flame graph
Flame graph: Shows which functions take the most time. Wider bars = more time.
perf (Linux)¶
System-wide profiling tool:
# Profile a program
$ perf record ./my_program
# View results
$ perf report
# Generate flame graph (with FlameGraph scripts)
$ perf script | stackcollapse-perf.pl | flamegraph.pl > flamegraph.svg
Benchmarking¶
Micro-benchmarks vs Realistic Workloads¶
Micro-benchmark: Testing a single function in isolation
import timeit
# Micro-benchmark: Which is faster, list comprehension or map?
list_comp = timeit.timeit('[x*2 for x in range(1000)]', number=10000)
map_func = timeit.timeit('list(map(lambda x: x*2, range(1000)))', number=10000)
print(f"List comprehension: {list_comp:.4f}s")
print(f"Map function: {map_func:.4f}s")
Realistic workload: Testing with real data and usage patterns
# Load actual user data from database
users = load_users_from_db()
# Measure time for realistic operation
start = time.time()
process_all_users(users)
end = time.time()
print(f"Processed {len(users)} users in {end - start:.2f}s")
Pitfall: Micro-benchmarks can be misleading. Optimizing for micro-benchmarks may not improve real-world performance.
Big-O in Practice: When Constant Factors Matter¶
Theory: O(n log n) is better than O(n²)
Practice: For small n, O(n²) with small constant factor may be faster than O(n log n) with large constant factor.
# Insertion sort: O(n²), but simple and fast for small lists
def insertion_sort(arr):
for i in range(1, len(arr)):
key = arr[i]
j = i - 1
while j >= 0 and arr[j] > key:
arr[j + 1] = arr[j]
j -= 1
arr[j + 1] = key
# Quicksort: O(n log n), but overhead for small lists
def quicksort(arr):
if len(arr) <= 1:
return arr
pivot = arr[len(arr) // 2]
left = [x for x in arr if x < pivot]
middle = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]
return quicksort(left) + middle + quicksort(right)
# Benchmark
import random
small_list = [random.randint(1, 100) for _ in range(20)]
# For small lists, insertion sort may be faster despite worse Big-O
Common Performance Issues¶
1. N+1 Query Problem¶
Problem: Making N database queries when 1 would suffice
# BAD: N+1 queries
def get_users_with_posts():
users = db.query("SELECT * FROM users") # 1 query
for user in users:
user.posts = db.query(f"SELECT * FROM posts WHERE user_id={user.id}") # N queries
return users
# GOOD: 1 query with JOIN
def get_users_with_posts():
return db.query("""
SELECT users.*, posts.*
FROM users
LEFT JOIN posts ON users.id = posts.user_id
""")
2. Unnecessary Re-renders (UI)¶
React example:
// BAD: Re-creates function on every render
function TodoList({ todos }) {
return (
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onDelete={() => deleteTodo(todo.id)} // New function every render!
/>
))}
</ul>
);
}
// GOOD: Memoized callback
function TodoList({ todos }) {
const handleDelete = useCallback((id) => {
deleteTodo(id);
}, []);
return (
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onDelete={handleDelete}
/>
))}
</ul>
);
}
3. Algorithmic Inefficiency¶
# BAD: O(n²) for finding duplicates
def has_duplicates(arr):
for i in range(len(arr)):
for j in range(i + 1, len(arr)):
if arr[i] == arr[j]:
return True
return False
# GOOD: O(n) using a set
def has_duplicates(arr):
return len(arr) != len(set(arr))
4. I/O Bottlenecks¶
Problem: Synchronous I/O blocks the entire program
// BAD: Synchronous, blocks the event loop
const fs = require('fs');
const data = fs.readFileSync('large-file.txt', 'utf8'); // Blocks!
console.log(data);
// GOOD: Asynchronous
const fs = require('fs').promises;
async function readFile() {
const data = await fs.readFile('large-file.txt', 'utf8');
console.log(data);
}
Python example:
# BAD: Sequential API calls (slow)
def fetch_all_users(user_ids):
users = []
for user_id in user_ids:
user = fetch_user(user_id) # Each call waits for previous
users.append(user)
return users
# GOOD: Concurrent API calls (fast)
import asyncio
async def fetch_all_users(user_ids):
tasks = [fetch_user(user_id) for user_id in user_ids]
users = await asyncio.gather(*tasks) # All calls run concurrently
return users
Summary¶
Debugging Principles: 1. Reproduce first ā Can't fix what you can't reproduce 2. Minimal example ā Strip away complexity 3. Form hypotheses ā Don't change things randomly 4. Use the right tool ā Debugger for complex flow, logs for production, profiler for performance 5. Understand before fixing ā Know why it's broken, not just how to make it work 6. Verify the fix ā Write a test to ensure it stays fixed
Performance Principles: 1. Measure first ā Don't optimize without profiling 2. Focus on hotspots ā 90% of time is spent in 10% of code 3. Algorithm beats micro-optimization ā O(n log n) vs O(n²) matters more than loop unrolling 4. Real workloads ā Benchmark with realistic data and usage patterns 5. Know when to stop ā "Fast enough" is good enough
Exercises¶
Exercise 1: Debug Buggy Code¶
Find and fix the bugs in this code:
def process_transactions(transactions):
total = 0
for transaction in transactions:
if transaction['type'] = 'debit':
total -= transaction['amount']
else:
total += transaction['amount']
return total
transactions = [
{'type': 'credit', 'amount': 100},
{'type': 'debit', 'amount': 50},
{'type': 'credit', 'amount': 200}
]
result = process_transactions(transactions)
print(f"Total: {result}")
Bugs to find: - Syntax error - Logic error (if any)
Exercise 2: Profile and Optimize¶
Profile this code and optimize it:
def find_common_elements(list1, list2):
common = []
for item1 in list1:
for item2 in list2:
if item1 == item2 and item1 not in common:
common.append(item1)
return common
# Test with large lists
list1 = list(range(10000))
list2 = list(range(5000, 15000))
result = find_common_elements(list1, list2)
Tasks: 1. Profile the code to identify bottlenecks 2. Optimize it (hint: use sets) 3. Measure the performance improvement
Exercise 3: Memory Leak Detection¶
This JavaScript code has a memory leak. Find and fix it:
class DataStore {
constructor() {
this.data = [];
this.subscribers = [];
}
addData(item) {
this.data.push(item);
this.notifySubscribers();
}
subscribe(callback) {
this.subscribers.push(callback);
}
notifySubscribers() {
this.subscribers.forEach(callback => callback(this.data));
}
}
// Usage
const store = new DataStore();
function createComponent() {
const component = {
render: (data) => {
console.log('Rendering with', data.length, 'items');
}
};
store.subscribe(component.render);
return component;
}
// Create and destroy components repeatedly
for (let i = 0; i < 100; i++) {
const component = createComponent();
// Component is no longer used, but...
}
Tasks:
1. Explain why this leaks memory
2. Implement an unsubscribe method
3. Ensure components are properly cleaned up
Exercise 4: Fix Race Condition¶
Fix the race condition in this code:
let balance = 100;
async function withdraw(amount) {
if (balance >= amount) {
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 100));
balance -= amount;
return true;
}
return false;
}
// Two withdrawals happen concurrently
Promise.all([
withdraw(60),
withdraw(60)
]).then(results => {
console.log('Withdrawals:', results);
console.log('Final balance:', balance); // Should be 100 or 40, never negative!
});
Tasks: 1. Explain the race condition 2. Fix it using a lock/mutex pattern 3. Verify it works correctly with concurrent operations
Navigation¶
Previous Lesson: 10_Testing_Fundamentals.md Next Lesson: 12_Concurrency_and_Parallelism.md