🛡️

Python try / except

How Python handles things going wrong — catching errors gracefully, keeping your program running, and what to do in the good path, the bad path, and always.

💥
Why try/except exists
Programs crash — unless you catch the crash first

When Python encounters an error at runtime — a file that doesn't exist, a number divided by zero, a network request that times out — it raises an exception. If nothing catches that exception, the program stops immediately and prints a traceback.

Without try/except

The error propagates up the call stack. If nothing catches it, the program crashes. For a web app this means a 500 error page for your user.

With try/except

You intercept the exception, handle it gracefully — log it, return a default value, show a friendly message — and the program keeps running.

A crash in action

Without protection, this code crashes on the second call:

# no protection — crashes on bad input def parse_age(text): return int(text) parse_age("42") # ✓ returns 42 parse_age("hello") # ✗ ValueError: invalid literal for int() with base 10: 'hello' # Traceback (most recent call last): ... # Program stops here.
Exceptions are objects. Every exception in Python is an instance of a class. ValueError, KeyError, FileNotFoundError are all built-in exception classes. You catch them by class name.
📐
The full syntax — try, except, else, finally
Four clauses, each with a distinct job

A try block can have up to four parts. Only try and except are required.

try: # code that might raise an exception result = risky_operation() except SomeError: # runs ONLY if SomeError was raised in the try block result = default_value else: # runs ONLY if NO exception was raised print("success!") finally: ## runs ALWAYS — whether an exception happened or not cleanup()

What each clause does

Clause When it runs Typical use
try: Always — it's the code you're protecting The risky operation
except: Only when an exception is raised in try Handle the error, set a default, log it
else: Only when NO exception was raised Code that should only run on success
finally: Always, no matter what Cleanup: close files, release locks, disconnect

Flow diagram

try: block risky code runs exception raised? YES except: block handle the error NO else: block success path finally: block always runs
Both paths (error and success) always reach finally
🎯
Catching specific exceptions
Name the error type you expect — don't catch everything blindly

You can catch a specific exception type by naming it after except. You can also capture the exception object itself with as e to inspect its message.

# catching a specific type try: age = int("hello") except ValueError: age = 0 # use a safe default # capturing the exception object with "as e" try: result = risky() except Exception as e: print(f"Something went wrong: {e}") ## e is the exception object; str(e) gives the error message # catching multiple types in one line (tuple) try: entry_id = int(request.args.get('entry_id', 0)) except (ValueError, TypeError): entry_id = 0 # either bad type or bad value → safe default
The last example is real J4H codeapp.py:529. A Twilio webhook sends entry_id as a query string. If it's missing or not a number, int() raises ValueError or TypeError. The except clause catches either and falls back to 0.

Common built-in exceptions

ExceptionWhen it's raised
ValueError Right type, wrong value — int("hello")
TypeError Wrong type entirely — int(None), "a" + 1
KeyError Dict key doesn't exist — d["missing"]
IndexError List index out of range — items[99] on a 3-item list
AttributeError Object doesn't have that attribute — None.strip()
FileNotFoundError File doesn't exist — open("missing.txt")
ZeroDivisionError Dividing by zero — 10 / 0
Exception The base class — catches any non-system exception
Avoid bare except: with no type. It catches everything including KeyboardInterrupt and SystemExit, which makes your program impossible to stop cleanly. Use except Exception at minimum if you want a broad catch.
🔀
Multiple except blocks
Handle different errors differently

You can stack multiple except clauses after one try. Python checks them top to bottom and runs the first one that matches. Put the most specific exceptions first — more specific classes before broader ones.

# multiple except blocks — most specific first try: data = load_patient_data(patient_id) age = int(data['age']) result = 100 / age except KeyError: ## 'age' key missing from data dict print("Patient record has no age field") except ValueError: ## int() failed — age wasn't a number print("Age field is not a valid number") except ZeroDivisionError: ## age was 0 print("Cannot divide by zero age") except Exception as e: ## anything else unexpected print(f"Unexpected error: {e}")
Order matters. Exception is the parent class of ValueError, KeyError, etc. If you put except Exception first, it matches everything and the specific clauses below it never run. Always put broad catches last.

The exception hierarchy (simplified)

BaseException KeyboardInterrupt SystemExit Exception ← catch this or below ValueError TypeError KeyError … and more
Catching a parent class also catches all its children — except Exception catches ValueError, KeyError, and everything else below it
🔁
else and finally in practice
The success-only path and the guaranteed cleanup path

else — only on success

else runs only if the try block completed without raising. It's useful when you want to do extra work after a successful operation but don't want that extra work to be covered by the except clause.

# else: runs only if try succeeded try: f = open("data.txt") except FileNotFoundError: print("File not found") else: contents = f.read() # only runs if open() succeeded f.close() print(contents)
Why use else instead of just putting the code in try? If you put contents = f.read() inside the try block and it raises an error, the except FileNotFoundError would catch it — even though the error has nothing to do with the file not existing. else keeps the error handling precise.

finally — guaranteed cleanup

finally runs no matter what — whether the try succeeded, whether an exception was caught, or even if the exception was not caught and is about to propagate up. It's the place for cleanup you can never skip.

# finally: runs no matter what happens conn = connect_to_database() try: result = conn.query("SELECT * FROM entries") except DatabaseError as e: print(f"Query failed: {e}") finally: conn.close() # always closes the connection # even if the query raised an unhandled exception
The modern alternative: Python's with statement (context managers) handles this automatically for files and connections. with open("data.txt") as f: guarantees the file closes when the block exits, exception or not. Internally, it uses the same finally mechanism.
🚀
Raising exceptions
Deliberately signaling that something is wrong

You can raise exceptions yourself with the raise keyword. This is how you signal an error from inside a function — the caller then decides whether to catch it.

# raise a built-in exception def set_pain_level(level): if not 1 <= level <= 10: raise ValueError(f"Pain level must be 1-10, got {level}") return level # re-raise inside an except block try: data = fetch_records() except Exception as e: logger.error(f"fetch failed: {e}") raise # bare raise — re-raises the same exception # logs it AND lets it propagate up

Custom exception classes

For larger applications you can define your own exception classes by subclassing Exception. This lets callers catch your specific error type without catching everything else.

# custom exception class class PatientNotFoundError(Exception): pass # inherits everything from Exception def get_patient(patient_id): patient = db.find(patient_id) if not patient: raise PatientNotFoundError(f"No patient with id {patient_id}") return patient # caller can be precise try: p = get_patient(999) except PatientNotFoundError: return jsonify({'error': 'Patient not found'}), 404 except Exception as e: return jsonify({'error': str(e)}), 500
🏥
Real examples from J4H
How try/except is used throughout this codebase

1. Optional feature that might not be configured

At startup, J4H tries to create the AI summarizer. If the API key isn't set, it raises ValueError. Rather than crashing, the app notes the missing feature and continues.

# app.py:61 — graceful degradation at startup try: summarizer = HealthSummarizer() except ValueError: summarizer = None print("Warning: ANTHROPIC_API_KEY not set. Summary features disabled.")

2. User input that might not be a number

When Twilio sends a webhook, the pain level comes in as a string. Wrapping int() in a try/except means a garbled input gives a safe default instead of a 500 error.

# app.py:536 — converting user input safely digits = request.form.get('Digits', '').strip() try: pain_level = int(digits) except ValueError: pain_level = 0 # non-numeric input → treat as 0

3. External service calls — always wrap in try/except

Every FHIR server call in fhir_service.py is wrapped. Networks fail, servers time out, responses are malformed. The except clause logs the error and returns an empty list so the rest of the app degrades gracefully instead of crashing.

# fhir_service.py:57 — external HTTP call def search_patients(self, name=None, limit=10): try: response = requests.get( f"{self.base_url}/Patient", params=params, headers=self.headers, timeout=10 ) response.raise_for_status() bundle = response.json() return [self._parse_patient(e['resource']) for e in bundle.get('entry', [])] except Exception as e: logger.error(f"Error searching patients: {e}") return [] # empty list, not a crash

4. Health dashboard — parallel checks, each isolated

The Admin health dashboard runs 7 service checks concurrently. Each check has its own try/except so one failing service doesn't prevent the others from reporting their status.

# app.py:1522 — isolated checks, each protected def check_db(): try: db.get_entries(limit=1) return 'database', {'status': 'ok', 'message': 'Connected'} except Exception as e: return 'database', {'status': 'error', 'message': str(e)}
Pattern you'll see everywhere in J4H: Flask route handlers wrap their logic in try / except Exception as e: return jsonify({'error': str(e)}), 500. This means an unexpected error returns a clean JSON error response instead of an HTML traceback page.

Exceptions are not failures — they're expected events.

Networks go down. Users type the wrong thing. Files get deleted. Writing try/except isn't pessimism — it's acknowledgment that the real world is messy and your code needs to handle it.

The goal is not to hide errors but to handle them at the right level: catch specifically, degrade gracefully, and always clean up after yourself.