Chapter 10: When Things Go Wrong (Error Handling)
Learning Objectives
- Understand that real-world software operates in a hostile environment.
- Learn how the CPU handles impossible instructions.
- Master the
try / except(ortry / catch) block. - Learn to read and trace Stack Traces.
Prerequisites
Chapter 5: Functions & the Call Stack.
Why Does This Exist?
If you write a calculator app, eventually a user will try to divide by zero.
If you write a web browser, eventually the user's WiFi will drop in the middle of a download.
Hardware fails. Networks drop. Users are unpredictable.
Without Error Handling, the moment one of these impossible scenarios happens, the Operating System steps in and violently assassinates your program. It crashes to the desktop.
We needed a way to tell the program: "If you hit an impossible situation, don't die. Alert me, gracefully recover, and keep running."
History
In older languages like C, functions returned special numbers to indicate failure. For example, if a function failed, it might return -1 instead of the actual data.
This led to a nightmare: programmers had to write an if result == -1: check after every single function call in the entire codebase. If they forgot even one, the program would silently process -1 as if it were real data, corrupting databases.
Modern languages invented Exceptions. Instead of returning a quiet failure code, Exceptions act like a loud alarm that automatically halts normal execution and forces the program to deal with the problem.
Mental Model
Think of the Call Stack as a skyscraper.
Your Main Program is on the top floor. It calls a function (goes down a floor). That function calls another function (goes down again). Now you are in the basement.
Suddenly, the basement function encounters a bomb (an error, like dividing by zero).
Instead of exploding, the function "throws" the bomb up to the next floor.
If that floor has a Bomb Defusal Unit (a try / except block), it catches the bomb, defuses it, and life continues.
If not, it throws it up again. If the bomb reaches the top floor and nobody caught it, the whole building explodes (the program crashes).
Internal Working
When the CPU attempts an impossible hardware instruction (like dividing by zero, or accessing memory outside array bounds), the CPU hardware generates a literal electrical "Interrupt".
The Operating System catches this interrupt. It looks at your program's Call Stack.
This process is called Stack Unwinding. The OS rapidly destroys function after function, traveling backwards through the stack, searching for a registered catch block.
If it finds one, it jumps the Instruction Pointer there. If it empties the entire stack, it prints the "Stack Trace" to the console and kills the process.
Syntax
In Python, we use try and except.
1try:
2 result = 10 / 0
3except ZeroDivisionError:
4 print("You cannot divide by zero!")
5 result = 0Token breakdown:
try:: Tells the OS, "I am setting up a Bomb Defusal squad for this block of code."10 / 0: Generates a hardware-level Exception. Normal execution stops instantly. Line 3 is skipped if there was one.except ZeroDivisionError:: The catcher. It specifically looks for bombs labeled "ZeroDivisionError".- The program handles the error, sets a safe default, and continues executing happily below line 5.
Visual Explanation
Tiny Example
1user_input = "abc"
2try:
3 age = int(user_input)
4except ValueError:
5 print("Please type a valid number.")
6 age = 18 # Default safe stateWalkthrough
- Line 3: The code tries to cast the string "abc" into the Integer mold. The CPU realizes this is impossible. It throws a
ValueError. - Line 4: The
exceptblock catches it. - Line 5-6: We alert the user and set a default age so the rest of the program doesn't crash later when it tries to use the
agevariable.
Common Mistakes
The Silent Assassin (Catch-All)
1try:
2 process_payment()
3except: # BAD: Catching absolutely everything
4 pass # BAD: Doing nothingWhy it fails: If process_payment() fails because of a typo in your code (a NameError), this block will catch the typo, silence it, and move on. The user will think they paid, but the database will be empty. You will spend weeks trying to find the bug.
The Fix: Always catch specific errors. except NetworkError:.
Debugging
When a program crashes, it spits out a massive wall of red text called a Stack Trace.
Junior engineers panic and ignore it.
Senior engineers read it from the BOTTOM UP.
The very last line tells you exactly what the error is (e.g., IndexError: list index out of range).
The line right above it tells you the exact file name and line number where the bomb exploded.
Mini Project
Time: 15 minutes.
Goal: The Safe Divider.
Write a function that takes two inputs and divides them. Wrap the math in a try/except block. Catch ZeroDivisionError and return "Cannot divide by zero". Catch TypeError (in case they pass strings) and return "Inputs must be numbers".
💡 See One Approach (Mini Project)
This is one valid solution — yours may differ.
# Safe divide with proper error handling
def safe_divide(a, b):
try:
result = a / b
return result
except ZeroDivisionError:
print(f" ⚠️ Cannot divide {a} by zero.")
return None
except TypeError as e:
print(f" ⚠️ Type error: {e}")
return None
test_cases = [
(10, 2),
(100, 0),
("ten", 2),
(7, 3),
]
print("=== Safe Divide ===")
for a, b in test_cases:
result = safe_divide(a, b)
if result is not None:
print(f" {a} / {b} = {result:.4f}")
Bigger Project
Time: 1 hour.
Goal: Robust Data Parser.
You have an array of mixed data: ["10", "20", "error", "30", None].
Write a for loop to iterate through this array. Try to convert each item to an integer and add it to a running total. Use try/except to gracefully skip any item that cannot be converted, printing a warning but allowing the loop to finish and print the final total.
💡 See One Approach (Bigger Project)
This is one valid solution — yours may differ.
# Robust file processor with layered error handling
import os
import json
class FileProcessingError(Exception):
"""Custom exception for our domain."""
pass
def read_json_file(filepath):
"""Layer 1: OS-level errors."""
if not os.path.exists(filepath):
raise FileNotFoundError(f"File not found: {filepath}")
if not filepath.endswith(".json"):
raise FileProcessingError(f"Expected a .json file, got: {filepath}")
with open(filepath, "r") as f:
return f.read()
def parse_json(content):
"""Layer 2: Parse errors."""
try:
return json.loads(content)
except json.JSONDecodeError as e:
raise FileProcessingError(f"Invalid JSON: {e}") from e
def validate_user_record(data):
"""Layer 3: Business logic errors."""
required = ["id", "name", "email"]
for field in required:
if field not in data:
raise FileProcessingError(f"Missing required field: '{field}'")
if "@" not in data.get("email", ""):
raise FileProcessingError(f"Invalid email: {data['email']}")
return True
def process_file(filepath):
"""Orchestrator — catches and categorises all errors."""
try:
content = read_json_file(filepath)
data = parse_json(content)
validate_user_record(data)
print(f" ✅ Successfully processed: {data['name']} <{data['email']}>")
return data
except FileNotFoundError as e:
print(f" ❌ [NOT_FOUND] {e}")
except FileProcessingError as e:
print(f" ❌ [PROCESS_ERROR] {e}")
except Exception as e:
# Catch-all: log and re-raise — never silently swallow unknown errors
print(f" 💥 [UNEXPECTED] {type(e).__name__}: {e}")
raise
return None
# Write test files and process them
import tempfile
test_cases = [
("valid.json", '{"id": 1, "name": "Tarun", "email": "tarun@example.com"}'),
("bad_json.json",'{"name": "broken JSON'),
("no_email.json",'{"id": 2, "name": "Alice"}'),
("bad_email.json",'{"id": 3, "name": "Bob", "email": "not-an-email"}'),
]
print("=== File Processor ===")
with tempfile.TemporaryDirectory() as tmpdir:
for filename, content in test_cases:
path = os.path.join(tmpdir, filename)
with open(path, "w") as f:
f.write(content)
for filename, _ in test_cases:
path = os.path.join(tmpdir, filename)
process_file(path)
# Test missing file
process_file("/nonexistent/path/file.json")
Production Usage
In massive cloud systems, servers fail constantly.
When an Amazon service tries to reach a database and the database is offline, it throws a TimeoutError. The code catches that error, waits 2 seconds, and tries again. This is called a "Retry Loop". Without Exceptions, the entire internet would crash every 5 seconds.
Best Practices
- Fail Fast: Don't use
try/exceptto hide bugs in your own logic. If your array is empty, fix the logic that made it empty, rather than catching the IndexError. Only catch things outside your control (network, file I/O, user input). - Finally: Most languages have a
finallyblock that runs no matter what (error or no error). Use this to clean up resources, like closing network connections.
Interview Questions
🟢 Easy:What does "throwing" or "raising" an exception mean?
🔍 Reveal Answer
🟡 Medium:Explain the purpose of a finally block.
🔍 Reveal Answer
try block succeeded or an except block was triggered.🔴 Hard:What is the performance cost of a try/except block?
🔍 Reveal Answer
try block has zero cost if no exception occurs. However, if an exception IS thrown, the Stack Unwinding process is extremely expensive and slow. Exceptions should not be used for normal control flow.Revision Sheet
- Exception: A loud alarm triggered by an impossible instruction.
- try/except: The bomb defusal squad that catches the alarm and prevents a crash.
- Stack Trace: The breadcrumb trail of exactly which functions were called leading up to the crash. Read it bottom-up.
- Rule: Only catch specific errors. Never use a blank
except:.
Connections
- Previous Chapter: Functions and the Call Stack are the infrastructure that makes Exception bubbling possible.
- Future Chapters: We will heavily rely on
try/exceptin the next chapter on File I/O, because reading files from a hard drive is highly prone to errors (missing files, bad permissions).