← Back to Course Index
Lesson 4 of 10

Error Handling

~15 min · try/except/else/finally, raising, custom exceptions

Ref
Primary Source
Official Docs — Errors and Exceptions (section 8)

Covers the exception hierarchy, handling, raising, and defining custom exceptions. Short and essential.

1 — try / except / else / finally

Python's error handling maps almost 1:1 to JS — with one powerful addition: the else clause, which runs only when no exception was raised.

JavaScript
try {
  const data = JSON.parse(rawInput);
  process(data);
} catch (err) {
  if (err instanceof SyntaxError) {
    console.error("Bad JSON:", err.message);
  } else {
    throw err; // re-throw
  }
} finally {
  cleanup();
}
Python
import json

try:
    data = json.loads(raw_input)
except json.JSONDecodeError as e:
    print(f"Bad JSON: {e}")
except Exception as e:
    raise  # re-raise current exception
else:
    # Runs ONLY if no exception was raised
    process(data)
finally:
    cleanup()  # always runs
💡 The else clause

The else block runs only when the try block succeeds. This is cleaner than putting the success code inside try, because it avoids accidentally catching exceptions from that code. JS has no equivalent — use it freely.

2 — Exception Hierarchy

Python exceptions are classes — all inherit from BaseException. Catch the most specific exception you can handle.

BaseException
 ├── SystemExit           ← sys.exit() raises this
 ├── KeyboardInterrupt    ← Ctrl+C
 └── Exception            ← catch-all for normal errors
      ├── ValueError       ← bad value (int("abc"))
      ├── TypeError        ← wrong type (1 + "a")
      ├── KeyError         ← missing dict key (d["nope"])
      ├── IndexError       ← list out of range (lst[99])
      ├── AttributeError   ← missing attribute (None.upper())
      ├── FileNotFoundError
      ├── ZeroDivisionError
      ├── NameError        ← undefined variable
      ├── ImportError
      └── RuntimeError

Catching multiple types

# Multiple except clauses — most specific first
try:
    result = risky()
except ValueError:
    handle_value_error()
except (TypeError, AttributeError):   # catch multiple in a tuple
    handle_type_issues()
except Exception as e:
    log_unexpected(e)
    raise
⚠ Never do this
try:
    do_something()
except:          # bare except — catches EVERYTHING including KeyboardInterrupt!
    pass         # silently swallows errors — dangerous

3 — Raising Exceptions

# Raise a built-in exception
def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b

# Re-raise the current exception (in an except block)
try:
    risky()
except Exception as e:
    log(e)
    raise        # re-raises the same exception with original traceback

# Raise with chaining (show cause)
try:
    int("abc")
except ValueError as e:
    raise RuntimeError("Config parse failed") from e

4 — Custom Exceptions

Create custom exceptions by subclassing Exception. This is how you build domain-specific error types.

class AppError(Exception):
    """Base exception for this application."""
    pass

class ValidationError(AppError):
    def __init__(self, field, message):
        self.field = field
        self.message = message
        super().__init__(f"{field}: {message}")

class NotFoundError(AppError):
    def __init__(self, resource, id):
        super().__init__(f"{resource} with id={id} not found")


# Usage
def get_user(user_id):
    user = db.find(user_id)
    if user is None:
        raise NotFoundError("User", user_id)
    return user

try:
    user = get_user(42)
except NotFoundError as e:
    print(e)   # "User with id=42 not found"
except AppError:
    print("Some app-level error")

5 — EAFP vs LBYL

Python culture prefers EAFP (Easier to Ask Forgiveness than Permission) over LBYL (Look Before You Leap). This is the opposite of many JS styles.

JS style — LBYL (check first)
// Check before acting
if (obj && obj.user && obj.user.name) {
  console.log(obj.user.name);
}

// Or optional chaining
console.log(obj?.user?.name);
Python style — EAFP (try it)
# Just try it — handle the error if it happens
try:
    print(obj["user"]["name"])
except (KeyError, TypeError):
    print("Not available")

# OR use .get() for dicts specifically
name = obj.get("user", {}).get("name")

EAFP is more Pythonic and often faster — you don't pay for the checks on the happy path. Use it when failures are genuinely exceptional.

🧠 Quiz

1. When does the else clause in a try/except run?

2. What does a bare raise (with no argument) do?

3. Accessing a missing key in a dict raises which exception?

4. What does EAFP mean in Python culture?

0/4

Questions answered correctly.