Skip to main content

šŸ›”ļø Lesson 7.1: Try, Catch, and Finally

Your program asks the user to type a number, and they type "banana." Your code tries to open a file that doesn't exist. You divide by zero. Without error handling, your program crashes — game over. With error handling, you catch the problem, respond gracefully, and keep running. This is one of the most practical skills in all of programming.

šŸŽÆ Learning Objectives

By the end of this lesson, you will be able to:

  • Explain what errors are and why they happen at runtime
  • Use try/catch (or try/except) to handle errors gracefully
  • Use finally to run cleanup code no matter what happens
  • Catch specific error types instead of catching everything
  • Access error information (message, type, stack trace)
  • Avoid common error-handling anti-patterns

Estimated Time: 50 minutes

Project: Build a safe user input system that never crashes

šŸ“‘ In This Lesson

What Are Errors?

There are two broad categories of problems in code:

Type When It Happens Example Can You Catch It?
Syntax Error Before the program runs Missing parenthesis, typo in keyword No — fix your code
Runtime Error While the program is running Divide by zero, file not found, bad user input Yes — with try/catch!

Runtime errors are also called exceptions — they're exceptional situations that interrupt normal program flow. When an exception happens and you don't handle it, your program crashes with an error message.

# Division by zero
result = 10 / 0       # ZeroDivisionError: division by zero

# Bad type conversion
number = int("banana")  # ValueError: invalid literal for int()

# Accessing a missing key
data = {"name": "Alice"}
print(data["age"])     # KeyError: 'age'

# Accessing a missing index
items = [1, 2, 3]
print(items[10])       # IndexError: list index out of range

# Using an undefined variable
print(unknown_var)     # NameError: name 'unknown_var' is not defined

# Opening a file that doesn't exist
f = open("nope.txt")  # FileNotFoundError: No such file or directory
// Accessing a property on null/undefined
let user = null;
console.log(user.name);    // TypeError: Cannot read properties of null

// Bad JSON parsing
JSON.parse("not json");    // SyntaxError: Unexpected token

// Calling a non-function
let x = 42;
x();                       // TypeError: x is not a function

// Using an undeclared variable (in strict mode)
"use strict";
console.log(unknownVar);   // ReferenceError: unknownVar is not defined

// Stack overflow (infinite recursion)
function loop() { loop(); }
loop();                    // RangeError: Maximum call stack size exceeded

// Note: JS division by zero gives Infinity, NOT an error
console.log(10 / 0);      // Infinity (no error thrown!)
// Division by zero (integers only — doubles give Infinity)
int result = 10 / 0;        // DivideByZeroException

// Bad type conversion
int number = int.Parse("banana");  // FormatException

// Null reference
string name = null;
Console.WriteLine(name.Length);    // NullReferenceException

// Index out of range
int[] items = { 1, 2, 3 };
Console.WriteLine(items[10]);      // IndexOutOfRangeException

// File not found
string text = File.ReadAllText("nope.txt");  // FileNotFoundException

// Invalid cast
object obj = "hello";
int bad = (int)obj;                // InvalidCastException

āš ļø JavaScript Quirk: Division by Zero

JavaScript does not throw an error when you divide by zero — it returns Infinity (or -Infinity or NaN for 0/0). Python and C# both throw exceptions. This is a fundamental difference in language design philosophy.

šŸŽ“ Instructor Note: Delivery Guidance

Run the "Common Runtime Errors" examples live and let students see the crash messages. The goal is to normalize error messages — they're not scary, they're helpful. Each one tells you exactly what went wrong and where. Point out that every error has a type (ZeroDivisionError, TypeError, etc.) — this becomes critical when we catch specific errors later. The JS division-by-zero quirk always gets a reaction; use it to reinforce that languages make different design choices.

Try/Catch: Your Safety Net

The try/catch pattern wraps risky code in a protective block. If an error occurs inside try, execution jumps immediately to the catch block instead of crashing the program.

flowchart TD A["Enter try block"] --> B{"Error occurs?"} B -->|No| C["Continue after try/catch"] B -->|Yes| D["Jump to catch block"] D --> E["Handle the error"] E --> C style A fill:#3b82f6,color:#fff style D fill:#ef4444,color:#fff style C fill:#22c55e,color:#fff
# Python uses try/except (not try/catch!)
try:
    number = int(input("Enter a number: "))
    result = 100 / number
    print(f"100 / {number} = {result}")
except:
    print("Something went wrong!")

print("Program continues running!")  # This ALWAYS runs

# --- Example run with "banana" ---
# Enter a number: banana
# Something went wrong!
# Program continues running!

# --- Example run with 0 ---
# Enter a number: 0
# Something went wrong!
# Program continues running!

# --- Example run with 5 ---
# Enter a number: 5
# 100 / 5 = 20.0
# Program continues running!
// JavaScript uses try/catch
try {
    let input = "banana";  // Simulating bad user input
    let number = Number(input);
    if (isNaN(number)) {
        throw new Error("Not a valid number!");
    }
    let result = 100 / number;
    console.log(`100 / ${number} = ${result}`);
} catch (error) {
    console.log("Something went wrong!");
}

console.log("Program continues running!");

// Note: JS needs manual validation for number conversion.
// Number("banana") returns NaN instead of throwing an error.
// We use 'throw' to create our own error — more on this in Lesson 20.

// --- Output ---
// Something went wrong!
// Program continues running!
// C# uses try/catch
try
{
    Console.Write("Enter a number: ");
    int number = int.Parse(Console.ReadLine()!);
    int result = 100 / number;
    Console.WriteLine($"100 / {number} = {result}");
}
catch (Exception e)
{
    Console.WriteLine("Something went wrong!");
}

Console.WriteLine("Program continues running!");

// --- Example run with "banana" ---
// Enter a number: banana
// Something went wrong!
// Program continues running!

Terminology Comparison

Concept šŸ Python ⚔ JavaScript šŸ”· C#
Try block try: try { } try { }
Catch block except: catch (error) { } catch (Exception e) { }
Error object name except Exception as e: catch (error) catch (Exception e)
Keyword for catch except catch catch

šŸ’” Python Says "except," Not "catch"

Python is the odd one out here — it uses try/except instead of try/catch. JavaScript and C# both use catch. This is a common gotcha when switching between languages. If you write catch in Python, you'll get a syntax error!

šŸŽ“ Instructor Note: Delivery Guidance

Run the basic example three times with different inputs: a valid number, zero, and "banana." Students see that the program never crashes — the catch/except block handles every problem. Emphasize the flow: if the error happens on line 2, lines 3-4 of the try block are skipped — execution jumps directly to the catch block. For JavaScript, note that Number("banana") doesn't throw; it returns NaN. This is why JS often needs explicit validation and throw — preview for Lesson 20.

Finally: Always Runs

The finally block runs no matter what — whether the try block succeeds, fails, or even if you return early. It's the perfect place for cleanup code: closing files, releasing connections, or resetting state.

flowchart TD A["Enter try block"] --> B{"Error occurs?"} B -->|No| C["finally block runs"] B -->|Yes| D["catch/except block"] D --> C C --> E["Continue program"] style A fill:#3b82f6,color:#fff style D fill:#ef4444,color:#fff style C fill:#f59e0b,color:#fff style E fill:#22c55e,color:#fff
# finally always runs — perfect for cleanup
def read_config(filename):
    file = None
    try:
        file = open(filename, "r")
        data = file.read()
        print(f"Read {len(data)} characters")
        return data
    except FileNotFoundError:
        print(f"File '{filename}' not found!")
        return None
    finally:
        # This runs even after a return statement!
        if file:
            file.close()
            print("File closed.")
        print("Cleanup complete.")

# Test with a missing file
result = read_config("settings.txt")
# File 'settings.txt' not found!
# Cleanup complete.

# --- With try/except/else/finally ---
try:
    number = int("42")
except ValueError:
    print("Not a number!")
else:
    # Only runs if NO exception occurred
    print(f"Converted successfully: {number}")
finally:
    # Always runs
    print("Done!")

# Output:
# Converted successfully: 42
# Done!
// finally always runs — perfect for cleanup
function readConfig(filename) {
    let connection = null;
    try {
        // Simulate opening a resource
        connection = { name: filename, open: true };
        console.log(`Reading ${filename}...`);

        // Simulate an error for missing files
        if (filename === "missing.txt") {
            throw new Error(`File '${filename}' not found!`);
        }

        console.log("File read successfully!");
        return "file contents";

    } catch (error) {
        console.log(`Error: ${error.message}`);
        return null;

    } finally {
        // This runs even after a return!
        if (connection) {
            connection.open = false;
            console.log("Connection closed.");
        }
        console.log("Cleanup complete.");
    }
}

readConfig("missing.txt");
// Reading missing.txt...
// Error: File 'missing.txt' not found!
// Connection closed.
// Cleanup complete.

readConfig("settings.txt");
// Reading settings.txt...
// File read successfully!
// Connection closed.
// Cleanup complete.
// finally always runs — perfect for cleanup
static string? ReadConfig(string filename)
{
    StreamReader? reader = null;
    try
    {
        reader = new StreamReader(filename);
        string data = reader.ReadToEnd();
        Console.WriteLine($"Read {data.Length} characters");
        return data;
    }
    catch (FileNotFoundException)
    {
        Console.WriteLine($"File '{filename}' not found!");
        return null;
    }
    finally
    {
        // This runs even after a return!
        reader?.Close();
        Console.WriteLine("Cleanup complete.");
    }
}

ReadConfig("settings.txt");
// File 'settings.txt' not found!
// Cleanup complete.

// --- try/catch/finally together ---
try
{
    int number = int.Parse("42");
    Console.WriteLine($"Converted: {number}");
}
catch (FormatException)
{
    Console.WriteLine("Not a number!");
}
finally
{
    Console.WriteLine("Done!");
}
// Converted: 42
// Done!

āœ… When to Use finally

  • Closing files or database connections
  • Releasing locks or resources
  • Resetting UI state (hiding a loading spinner)
  • Logging that an operation completed (success or failure)

finally runs even if you return from inside the try or catch blocks — it's truly guaranteed.

šŸ’” Python Bonus: else

Python has an extra clause — else — that runs only when no exception occurred. JavaScript and C# don't have this; you'd put that code at the end of the try block instead. The Python pattern is: try → except → else → finally.

Catching Specific Errors

Catching all errors with a bare except or catch is like wearing a blindfold — you know something went wrong, but not what. Catching specific error types lets you respond appropriately to each situation.

def safe_divide(a, b):
    try:
        result = a / b
        return result
    except ZeroDivisionError:
        print("Can't divide by zero!")
        return None
    except TypeError:
        print(f"Can't divide {type(a).__name__} by {type(b).__name__}!")
        return None

print(safe_divide(10, 3))      # 3.333...
print(safe_divide(10, 0))      # Can't divide by zero! → None
print(safe_divide("10", 3))    # Can't divide str by int! → None

# --- Multiple except blocks: order matters ---
def get_value(data, key, index):
    try:
        return data[key][index]
    except KeyError:
        print(f"Key '{key}' not found in data")
    except IndexError:
        print(f"Index {index} is out of range")
    except TypeError:
        print(f"Wrong type — can't index into this")

scores = {"math": [95, 87, 92], "english": [88, 91]}

get_value(scores, "math", 1)      # Returns 87
get_value(scores, "science", 0)   # Key 'science' not found in data
get_value(scores, "math", 10)     # Index 10 is out of range

# --- Catching multiple types in one block ---
try:
    value = int("banana")
except (ValueError, TypeError) as e:
    print(f"Conversion error: {e}")
# Conversion error: invalid literal for int() with base 10: 'banana'
// JS catch gets ONE block — use instanceof to differentiate
function safeParse(jsonString) {
    try {
        return JSON.parse(jsonString);
    } catch (error) {
        if (error instanceof SyntaxError) {
            console.log("Invalid JSON format!");
        } else if (error instanceof TypeError) {
            console.log("Wrong input type!");
        } else {
            console.log(`Unexpected error: ${error.message}`);
        }
        return null;
    }
}

safeParse('{"name": "Alice"}');  // Returns { name: "Alice" }
safeParse("not json at all");    // Invalid JSON format! → null
safeParse(undefined);            // Wrong input type! → null

// --- Common pattern: validate then throw ---
function getElement(arr, index) {
    try {
        if (!Array.isArray(arr)) {
            throw new TypeError("Expected an array");
        }
        if (index < 0 || index >= arr.length) {
            throw new RangeError(`Index ${index} out of bounds (0-${arr.length - 1})`);
        }
        return arr[index];
    } catch (error) {
        if (error instanceof TypeError) {
            console.log(`Type error: ${error.message}`);
        } else if (error instanceof RangeError) {
            console.log(`Range error: ${error.message}`);
        }
        return undefined;
    }
}

getElement([10, 20, 30], 1);    // Returns 20
getElement("not array", 0);     // Type error: Expected an array
getElement([10, 20], 5);        // Range error: Index 5 out of bounds (0-1)
// C# supports multiple catch blocks with specific types
static int? SafeConvert(string input)
{
    try
    {
        return int.Parse(input);
    }
    catch (FormatException)
    {
        Console.WriteLine($"'{input}' is not a valid number!");
        return null;
    }
    catch (OverflowException)
    {
        Console.WriteLine($"'{input}' is too large or too small!");
        return null;
    }
    catch (ArgumentNullException)
    {
        Console.WriteLine("Input cannot be null!");
        return null;
    }
}

SafeConvert("42");                // Returns 42
SafeConvert("banana");            // 'banana' is not a valid number!
SafeConvert("99999999999999");    // Too large or too small!

// --- Order matters: most specific FIRST ---
try
{
    int[] items = { 1, 2, 3 };
    Console.WriteLine(items[10]);
}
catch (IndexOutOfRangeException e)
{
    // Specific catch — handles this exact error type
    Console.WriteLine($"Bad index: {e.Message}");
}
catch (Exception e)
{
    // General catch — catches anything else
    Console.WriteLine($"Something else went wrong: {e.Message}");
}
// C# enforces order: specific exceptions before general ones.
// Putting catch(Exception) first would be a compile error!

Error Type Reference

Situation šŸ Python ⚔ JavaScript šŸ”· C#
Bad number conversion ValueError Returns NaN (no error) FormatException
Divide by zero ZeroDivisionError Returns Infinity DivideByZeroException
Null/None access AttributeError TypeError NullReferenceException
Index out of range IndexError Returns undefined IndexOutOfRangeException
Missing key/property KeyError Returns undefined KeyNotFoundException
File not found FileNotFoundError Depends on API FileNotFoundException

āš ļø JavaScript Is the Lenient One

Notice how many situations in JavaScript return undefined, NaN, or Infinity instead of throwing errors. JavaScript was designed to "keep going" rather than crash — but this means bugs can hide silently. Python and C# are stricter: they throw immediately so you know exactly where the problem is.

šŸŽ“ Instructor Note: Delivery Guidance

The error type reference table is a key resource — encourage students to bookmark this lesson. Walk through the JavaScript column and point out how many things silently return undefined or NaN instead of throwing. This is why JS developers rely heavily on manual validation and throw. For C#, emphasize that the compiler enforces catch order: specific exceptions must come before general ones. Python allows any order but best practice matches C#'s approach.

Accessing Error Information

When you catch an error, you get an error object with details about what went wrong. This is invaluable for logging, debugging, and showing useful messages to users.

try:
    result = 10 / 0
except ZeroDivisionError as e:
    # The error object
    print(f"Type:    {type(e).__name__}")   # ZeroDivisionError
    print(f"Message: {e}")                  # division by zero
    print(f"Args:    {e.args}")             # ('division by zero',)

print()

# Get the full traceback (stack trace)
import traceback

try:
    data = {"a": [1, 2, 3]}
    value = data["b"][0]
except KeyError as e:
    print(f"Error: Key {e} not found")
    print("--- Stack Trace ---")
    traceback.print_exc()

# --- Output ---
# Error: Key 'b' not found
# --- Stack Trace ---
# Traceback (most recent call last):
#   File "example.py", line 13, in <module>
#     value = data["b"][0]
# KeyError: 'b'
try {
    JSON.parse("not valid json");
} catch (error) {
    // The error object
    console.log(`Name:    ${error.name}`);     // SyntaxError
    console.log(`Message: ${error.message}`);  // Unexpected token...
    console.log(`Stack:   ${error.stack}`);    // Full stack trace

    // error.stack includes the name, message, AND call locations
}

// --- All JS error types have: name, message, stack ---

try {
    null.toString();
} catch (error) {
    console.log(error.name);     // TypeError
    console.log(error.message);  // Cannot read properties of null
}

try {
    undeclaredVar;
} catch (error) {
    console.log(error.name);     // ReferenceError
    console.log(error.message);  // undeclaredVar is not defined
}
try
{
    int result = int.Parse("banana");
}
catch (FormatException e)
{
    // The exception object
    Console.WriteLine($"Type:    {e.GetType().Name}");   // FormatException
    Console.WriteLine($"Message: {e.Message}");          // Input string was...
    Console.WriteLine($"Source:  {e.Source}");            // System.Private.CoreLib
    Console.WriteLine($"Stack:\n{e.StackTrace}");        // Full stack trace
}

// --- InnerException: errors caused by other errors ---
try
{
    try
    {
        int.Parse("banana");
    }
    catch (FormatException inner)
    {
        // Wrap the original error with more context
        throw new InvalidOperationException("Config parsing failed", inner);
    }
}
catch (InvalidOperationException e)
{
    Console.WriteLine($"Outer: {e.Message}");  // Config parsing failed
    Console.WriteLine($"Inner: {e.InnerException?.Message}");
    // Inner: The input string 'banana' was not in a correct format.
}
Property šŸ Python ⚔ JavaScript šŸ”· C#
Error type/name type(e).__name__ error.name e.GetType().Name
Error message str(e) error.message e.Message
Stack trace traceback.print_exc() error.stack e.StackTrace
Wrapped inner error e.__cause__ error.cause e.InnerException

Anti-Patterns to Avoid

Error handling can go wrong in subtle ways. Here are the most common mistakes beginners make — and what to do instead.

# āŒ BAD: Silently swallowing errors
try:
    result = dangerous_operation()
except:
    pass  # Errors vanish — bugs become invisible!

# āœ… GOOD: At minimum, log the error
try:
    result = dangerous_operation()
except Exception as e:
    print(f"Error: {e}")  # Now you know something went wrong
    # Or: logging.error(f"Operation failed: {e}")
// āŒ BAD: Silently swallowing errors
try {
    result = dangerousOperation();
} catch (error) {
    // Nothing here — bugs become invisible!
}

// āœ… GOOD: At minimum, log the error
try {
    result = dangerousOperation();
} catch (error) {
    console.error("Operation failed:", error.message);
}
// āŒ BAD: Silently swallowing errors
try
{
    DangerousOperation();
}
catch { }  // Errors vanish!

// āœ… GOOD: At minimum, log the error
try
{
    DangerousOperation();
}
catch (Exception e)
{
    Console.Error.WriteLine($"Operation failed: {e.Message}");
}
# āŒ BAD: Catching everything hides real bugs
try:
    users = load_users()      # Might raise FileNotFoundError
    first = users[0]          # Might raise IndexError
    name = first.name         # Might raise AttributeError
    print(greet(name))        # Might raise TypeError
except:
    print("Something went wrong")  # But WHAT went wrong?!

# āœ… GOOD: Catch only what you expect
try:
    users = load_users()
except FileNotFoundError:
    print("Users file not found — using defaults")
    users = []

# Let unexpected errors (IndexError, AttributeError) crash
# so you can find and fix the real bug!
// āŒ BAD: Catching everything hides bugs
try {
    let users = loadUsers();
    let first = users[0];
    let name = first.name;
    console.log(greet(name));
} catch (error) {
    console.log("Something went wrong");  // But WHAT?!
}

// āœ… GOOD: Wrap only the risky part, handle specifically
let users;
try {
    users = loadUsers();  // Only wrap the part that might fail
} catch (error) {
    console.log("Could not load users — using defaults");
    users = [];
}

// Other bugs will throw naturally and you'll see them
// āŒ BAD: Catching the base Exception hides bugs
try
{
    var users = LoadUsers();
    var first = users[0];
    Console.WriteLine(Greet(first.Name));
}
catch (Exception)
{
    Console.WriteLine("Something went wrong");  // WHAT?!
}

// āœ… GOOD: Catch only what you expect
List<User> users;
try
{
    users = LoadUsers();
}
catch (FileNotFoundException)
{
    Console.WriteLine("Users file not found — using defaults");
    users = new List<User>();
}
// IndexOutOfRangeException, NullReferenceException etc. 
// will still crash — and that's GOOD because those are bugs!
# āŒ BAD: Using exceptions like if/else
def find_user(users, name):
    try:
        for user in users:
            if user.name == name:
                return user
        raise ValueError("Not found")  # Don't do this!
    except ValueError:
        return None

# āœ… GOOD: Just use normal control flow
def find_user(users, name):
    for user in users:
        if user.name == name:
            return user
    return None  # Simple and clear
// āŒ BAD: Using exceptions like if/else
function findUser(users, name) {
    try {
        for (let user of users) {
            if (user.name === name) return user;
        }
        throw new Error("Not found");
    } catch (error) {
        return null;
    }
}

// āœ… GOOD: Just use normal control flow
function findUser(users, name) {
    return users.find(u => u.name === name) || null;
}
// āŒ BAD: Using exceptions like if/else
static User? FindUser(List<User> users, string name)
{
    try
    {
        foreach (var user in users)
            if (user.Name == name) return user;
        throw new Exception("Not found");
    }
    catch { return null; }
}

// āœ… GOOD: Just use normal control flow
static User? FindUser(List<User> users, string name)
{
    return users.FirstOrDefault(u => u.Name == name);
}

šŸ’” The Golden Rule of Error Handling

Use exceptions for exceptional situations — things that shouldn't normally happen (file missing, network down, corrupt data). Don't use them as a substitute for if/else. Exceptions are slower than conditionals and make code harder to follow when used for normal logic.

Real-World Pattern: Retry Logic

One of the most useful patterns you'll use in real applications: retrying an operation that might fail temporarily (like a network request or database connection).

import random
import time

def unreliable_service():
    """Simulates a service that fails 60% of the time"""
    if random.random() < 0.6:
        raise ConnectionError("Server unavailable")
    return {"status": "ok", "data": [1, 2, 3]}

def fetch_with_retry(max_attempts=3, delay=1):
    for attempt in range(1, max_attempts + 1):
        try:
            result = unreliable_service()
            print(f"āœ… Success on attempt {attempt}!")
            return result
        except ConnectionError as e:
            print(f"āŒ Attempt {attempt} failed: {e}")
            if attempt < max_attempts:
                print(f"   Retrying in {delay} second(s)...")
                time.sleep(delay)
            else:
                print("šŸ’€ All attempts failed!")
                return None

# Run it
data = fetch_with_retry()
if data:
    print(f"Got data: {data}")
else:
    print("Could not reach the service")
function unreliableService() {
    if (Math.random() < 0.6) {
        throw new Error("Server unavailable");
    }
    return { status: "ok", data: [1, 2, 3] };
}

function fetchWithRetry(maxAttempts = 3, delay = 1000) {
    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
        try {
            let result = unreliableService();
            console.log(`āœ… Success on attempt ${attempt}!`);
            return result;
        } catch (error) {
            console.log(`āŒ Attempt ${attempt} failed: ${error.message}`);
            if (attempt < maxAttempts) {
                console.log(`   Retrying...`);
                // In real code, you'd use async/await with a delay
            } else {
                console.log("šŸ’€ All attempts failed!");
                return null;
            }
        }
    }
}

let data = fetchWithRetry();
if (data) {
    console.log("Got data:", data);
} else {
    console.log("Could not reach the service");
}
static Random rng = new();

static Dictionary<string, object> UnreliableService()
{
    if (rng.NextDouble() < 0.6)
        throw new HttpRequestException("Server unavailable");
    return new() { ["status"] = "ok", ["data"] = new[] { 1, 2, 3 } };
}

static Dictionary<string, object>? FetchWithRetry(int maxAttempts = 3, int delayMs = 1000)
{
    for (int attempt = 1; attempt <= maxAttempts; attempt++)
    {
        try
        {
            var result = UnreliableService();
            Console.WriteLine($"āœ… Success on attempt {attempt}!");
            return result;
        }
        catch (HttpRequestException e)
        {
            Console.WriteLine($"āŒ Attempt {attempt} failed: {e.Message}");
            if (attempt < maxAttempts)
            {
                Console.WriteLine($"   Retrying in {delayMs}ms...");
                Thread.Sleep(delayMs);
            }
            else
            {
                Console.WriteLine("šŸ’€ All attempts failed!");
                return null;
            }
        }
    }
    return null;
}

var data = FetchWithRetry();
Console.WriteLine(data != null ? "Got data!" : "Could not reach the service");
šŸŽ“ Instructor Note: Delivery Guidance

The retry pattern is a great way to end the lesson because it ties everything together: try/catch, specific error types, loops, and real-world relevance. Run it multiple times to show different outcomes — sometimes it succeeds on attempt 1, sometimes attempt 3, sometimes all attempts fail. Students see that error handling isn't just about preventing crashes, it's about building resilient software. For advanced students, mention exponential backoff (doubling the delay each retry) as a common production pattern.

Exercises

šŸ‹ļø Exercise 1: Safe Calculator

Objective: Build a calculator function that handles every possible error:

  • Takes two values and an operator (+, -, *, /)
  • Handles invalid numbers (non-numeric input)
  • Handles division by zero
  • Handles invalid operators
  • Returns the result on success or an error message on failure
  • Wrap it in a loop that keeps asking for input until the user types "quit"
āœ… Solution
def safe_calculate(a_str, op, b_str):
    """Calculate with full error handling."""
    # Validate numbers
    try:
        a = float(a_str)
    except ValueError:
        return f"Error: '{a_str}' is not a valid number"

    try:
        b = float(b_str)
    except ValueError:
        return f"Error: '{b_str}' is not a valid number"

    # Perform operation
    try:
        if op == "+":
            return f"{a} + {b} = {a + b}"
        elif op == "-":
            return f"{a} - {b} = {a - b}"
        elif op == "*":
            return f"{a} * {b} = {a * b}"
        elif op == "/":
            return f"{a} / {b} = {a / b}"
        else:
            return f"Error: Unknown operator '{op}'"
    except ZeroDivisionError:
        return "Error: Cannot divide by zero!"

# Interactive loop
print("=== Safe Calculator ===")
print("Type 'quit' to exit\n")

while True:
    expression = input("Enter calculation (e.g., 5 + 3): ").strip()
    if expression.lower() == "quit":
        print("Goodbye!")
        break

    parts = expression.split()
    if len(parts) != 3:
        print("Error: Use format 'number operator number' (e.g., 5 + 3)")
        continue

    result = safe_calculate(parts[0], parts[1], parts[2])
    print(result)
function safeCalculate(aStr, op, bStr) {
    let a = Number(aStr);
    let b = Number(bStr);

    if (isNaN(a)) return `Error: '${aStr}' is not a valid number`;
    if (isNaN(b)) return `Error: '${bStr}' is not a valid number`;

    switch (op) {
        case "+": return `${a} + ${b} = ${a + b}`;
        case "-": return `${a} - ${b} = ${a - b}`;
        case "*": return `${a} * ${b} = ${a * b}`;
        case "/":
            if (b === 0) return "Error: Cannot divide by zero!";
            return `${a} / ${b} = ${a / b}`;
        default:
            return `Error: Unknown operator '${op}'`;
    }
}

// Test cases (since we can't easily loop with prompt)
console.log(safeCalculate("10", "+", "3"));     // 10 + 3 = 13
console.log(safeCalculate("10", "/", "0"));     // Error: Cannot divide by zero!
console.log(safeCalculate("banana", "+", "3")); // Error: 'banana' is not valid
console.log(safeCalculate("10", "^", "3"));     // Error: Unknown operator '^'

// In a browser, you could use:
// let input = prompt("Enter calculation (e.g., 5 + 3):");
// let parts = input.split(" ");
// console.log(safeCalculate(parts[0], parts[1], parts[2]));
static string SafeCalculate(string aStr, string op, string bStr)
{
    double a, b;

    try { a = double.Parse(aStr); }
    catch (FormatException) { return $"Error: '{aStr}' is not a valid number"; }

    try { b = double.Parse(bStr); }
    catch (FormatException) { return $"Error: '{bStr}' is not a valid number"; }

    try
    {
        return op switch
        {
            "+" => $"{a} + {b} = {a + b}",
            "-" => $"{a} - {b} = {a - b}",
            "*" => $"{a} * {b} = {a * b}",
            "/" => b == 0
                ? "Error: Cannot divide by zero!"
                : $"{a} / {b} = {a / b}",
            _ => $"Error: Unknown operator '{op}'"
        };
    }
    catch (Exception e)
    {
        return $"Unexpected error: {e.Message}";
    }
}

// Interactive loop
Console.WriteLine("=== Safe Calculator ===");
Console.WriteLine("Type 'quit' to exit\n");

while (true)
{
    Console.Write("Enter calculation (e.g., 5 + 3): ");
    string? input = Console.ReadLine()?.Trim();
    if (input?.ToLower() == "quit") { Console.WriteLine("Goodbye!"); break; }

    string[] parts = input?.Split(' ') ?? Array.Empty<string>();
    if (parts.Length != 3)
    {
        Console.WriteLine("Error: Use format 'number operator number'");
        continue;
    }

    Console.WriteLine(SafeCalculate(parts[0], parts[1], parts[2]));
}

šŸ‹ļø Exercise 2: File Data Processor

Objective: Build a function that reads a "data file" (simulated) and processes numbers:

  • Accept a list of strings (simulating lines from a file)
  • Convert each line to a number
  • Skip lines that can't be converted (log a warning)
  • Return the sum, average, min, and max of the valid numbers
  • Handle the case where NO lines are valid (don't crash on empty list)
  • Use finally to print a processing summary
āœ… Solution
def process_data(lines):
    """Process a list of strings into numeric statistics."""
    numbers = []
    errors = []

    try:
        for i, line in enumerate(lines, 1):
            try:
                number = float(line.strip())
                numbers.append(number)
            except ValueError:
                errors.append(f"Line {i}: '{line.strip()}' is not a number")

        if not numbers:
            raise ValueError("No valid numbers found in data!")

        return {
            "sum": sum(numbers),
            "average": sum(numbers) / len(numbers),
            "min": min(numbers),
            "max": max(numbers),
            "count": len(numbers)
        }

    except ValueError as e:
        print(f"āŒ Processing failed: {e}")
        return None

    finally:
        print(f"\nšŸ“Š Processing Summary:")
        print(f"   Total lines:  {len(lines)}")
        print(f"   Valid numbers: {len(numbers)}")
        print(f"   Errors:        {len(errors)}")
        for err in errors:
            print(f"   āš ļø {err}")

# Test with mixed data
data = ["42", "17.5", "banana", "99", "", "hello", "3.14", "-7"]
result = process_data(data)

if result:
    print(f"\nāœ… Results:")
    print(f"   Sum:     {result['sum']}")
    print(f"   Average: {result['average']:.2f}")
    print(f"   Min:     {result['min']}")
    print(f"   Max:     {result['max']}")

# Test with all invalid data
print("\n" + "=" * 40)
process_data(["abc", "def", "ghi"])
function processData(lines) {
    let numbers = [];
    let errors = [];

    try {
        lines.forEach((line, i) => {
            let trimmed = line.trim();
            let num = Number(trimmed);
            if (trimmed === "" || isNaN(num)) {
                errors.push(`Line ${i + 1}: '${trimmed}' is not a number`);
            } else {
                numbers.push(num);
            }
        });

        if (numbers.length === 0) {
            throw new Error("No valid numbers found in data!");
        }

        return {
            sum: numbers.reduce((a, b) => a + b, 0),
            average: numbers.reduce((a, b) => a + b, 0) / numbers.length,
            min: Math.min(...numbers),
            max: Math.max(...numbers),
            count: numbers.length
        };

    } catch (error) {
        console.log(`āŒ Processing failed: ${error.message}`);
        return null;

    } finally {
        console.log(`\nšŸ“Š Processing Summary:`);
        console.log(`   Total lines:  ${lines.length}`);
        console.log(`   Valid numbers: ${numbers.length}`);
        console.log(`   Errors:        ${errors.length}`);
        errors.forEach(err => console.log(`   āš ļø ${err}`));
    }
}

let data = ["42", "17.5", "banana", "99", "", "hello", "3.14", "-7"];
let result = processData(data);

if (result) {
    console.log(`\nāœ… Results:`);
    console.log(`   Sum:     ${result.sum}`);
    console.log(`   Average: ${result.average.toFixed(2)}`);
    console.log(`   Min:     ${result.min}`);
    console.log(`   Max:     ${result.max}`);
}
static Dictionary<string, double>? ProcessData(string[] lines)
{
    var numbers = new List<double>();
    var errors = new List<string>();

    try
    {
        for (int i = 0; i < lines.Length; i++)
        {
            string trimmed = lines[i].Trim();
            try
            {
                numbers.Add(double.Parse(trimmed));
            }
            catch (FormatException)
            {
                errors.Add($"Line {i + 1}: '{trimmed}' is not a number");
            }
        }

        if (numbers.Count == 0)
            throw new InvalidOperationException("No valid numbers found!");

        return new Dictionary<string, double>
        {
            ["sum"] = numbers.Sum(),
            ["average"] = numbers.Average(),
            ["min"] = numbers.Min(),
            ["max"] = numbers.Max(),
            ["count"] = numbers.Count
        };
    }
    catch (InvalidOperationException e)
    {
        Console.WriteLine($"āŒ Processing failed: {e.Message}");
        return null;
    }
    finally
    {
        Console.WriteLine($"\nšŸ“Š Processing Summary:");
        Console.WriteLine($"   Total lines:  {lines.Length}");
        Console.WriteLine($"   Valid numbers: {numbers.Count}");
        Console.WriteLine($"   Errors:        {errors.Count}");
        errors.ForEach(err => Console.WriteLine($"   āš ļø {err}"));
    }
}

string[] data = { "42", "17.5", "banana", "99", "", "hello", "3.14", "-7" };
var result = ProcessData(data);

if (result != null)
{
    Console.WriteLine($"\nāœ… Results:");
    Console.WriteLine($"   Sum:     {result["sum"]}");
    Console.WriteLine($"   Average: {result["average"]:F2}");
    Console.WriteLine($"   Min:     {result["min"]}");
    Console.WriteLine($"   Max:     {result["max"]}");
}
šŸŽ“ Instructor Note: Delivery Guidance

Exercise 1 (Safe Calculator) is practical and relatable — everyone has used a calculator. It covers multiple error types in one function and uses a loop for repeated input. Exercise 2 (File Data Processor) is more realistic — processing messy data with a mix of valid and invalid entries is something every developer encounters. The finally block printing a summary regardless of success/failure demonstrates real-world logging patterns. Challenge fast students: add support for operators like % (modulo) and ** (power) to the calculator, or add standard deviation to the data processor.

Summary

šŸŽ‰ Key Takeaways

  • Runtime errors (exceptions) happen during execution — they're different from syntax errors
  • try/catch (Python: try/except) wraps risky code so errors don't crash your program
  • finally always runs — use it for cleanup (closing files, connections, resetting state)
  • Catch specific errors — don't blindly catch everything or you'll hide real bugs
  • Error objects give you the type, message, and stack trace for debugging
  • Anti-patterns: empty catch blocks, catching too broadly, using exceptions for normal logic
  • JavaScript is more lenient (returns NaN/undefined instead of throwing) — be extra vigilant with validation

šŸš€ What's Next?

Now that you can catch errors, what about creating your own? In the next lesson, we'll learn to define custom errors and exceptions — giving your code meaningful, specific error types instead of generic messages.

šŸŽÆ Quick Check

Question 1: What keyword does Python use instead of "catch"?

Question 2: When does the finally block run?

Question 3: What happens in JavaScript when you divide by zero?