Foundations taught try/except Exception as e — catch everything. Today: notice that exceptions have classes, and the class tells you what went wrong.
try:
something()
except Exception as e:
print(f"failed: {type(e).__name__}")
# 'KeyError' — your code looked up a missing dict key
# 'ConnectionError' — network couldn't reach the API
# 'RuntimeError' — Composio raised because the tool said no
# 'TimeoutError' — call took too longDifferent classes deserve different responses. Network errors retry. Auth errors don't. Bad-arg errors fix the call. Today: just log the class.
And type(e).__name__ is the string name?
Yes — Foundations week 1. type(e) is the class object; .__name__ is the human-readable class name. "KeyError", "ValueError", etc.
Why is the class enough — won't I want the message too?
Class first, message second. Class is one short token ("KeyError"); message can be paragraph-long. Logs you'll grep need stable, short tokens. We'll add structured logging in week 2 (day 13) — log("step", error_class="KeyError", message=str(e)). For now: the class is the high-signal piece.
Trigger three different errors deliberately so you see what they look like.
A blanket except Exception as e: print("failed") tells you nothing. The same handler fires for:
The class — ConnectionError, PermissionError, ValueError, RuntimeError with a 429 in the message — tells you which.
try:
risky_call()
except Exception as e:
print(f"caught: {type(e).__name__}: {e}")type(e).__name__ is short and stable. str(e) is the message; useful but variable. Log both.
| Class | Likely cause | Action |
|---|---|---|
KeyError | dict[key] missed; you assumed a field was there | use .get(key, default) |
IndexError | list[i] but list is shorter | guard with if items: |
TypeError | passing wrong type — len(None) | check upstream .get defaults |
ValueError | right type, bad value | validate inputs |
ConnectionError | network unreachable | retry with backoff |
TimeoutError | call exceeded deadline | retry with longer timeout |
PermissionError | auth failed | reconnect, don't retry |
RuntimeError (Composio) | tool said no — message has details | inspect str(e) |
Once you know what classes to expect, catch them by name:
try:
risky_call()
except ConnectionError:
retry()
except PermissionError:
abort_and_alert()
except Exception as e:
print(f"unexpected: {type(e).__name__}: {e}")
raiseThe specific handlers fire first; the catch-all is the fallback. Always have a catch-all Exception handler — surprise classes shouldn't crash silently.
For today's lesson, force three different classes to fire:
# KeyError
d = {}
try:
d["missing"]
except Exception as e:
print(type(e).__name__)
# ZeroDivisionError
try:
1 / 0
except Exception as e:
print(type(e).__name__)
# ValueError
try:
int("not a number")
except Exception as e:
print(type(e).__name__)Three errors deliberately raised; three class names printed. The shape generalizes to any try/except.
Foundations taught try/except Exception as e — catch everything. Today: notice that exceptions have classes, and the class tells you what went wrong.
try:
something()
except Exception as e:
print(f"failed: {type(e).__name__}")
# 'KeyError' — your code looked up a missing dict key
# 'ConnectionError' — network couldn't reach the API
# 'RuntimeError' — Composio raised because the tool said no
# 'TimeoutError' — call took too longDifferent classes deserve different responses. Network errors retry. Auth errors don't. Bad-arg errors fix the call. Today: just log the class.
And type(e).__name__ is the string name?
Yes — Foundations week 1. type(e) is the class object; .__name__ is the human-readable class name. "KeyError", "ValueError", etc.
Why is the class enough — won't I want the message too?
Class first, message second. Class is one short token ("KeyError"); message can be paragraph-long. Logs you'll grep need stable, short tokens. We'll add structured logging in week 2 (day 13) — log("step", error_class="KeyError", message=str(e)). For now: the class is the high-signal piece.
Trigger three different errors deliberately so you see what they look like.
A blanket except Exception as e: print("failed") tells you nothing. The same handler fires for:
The class — ConnectionError, PermissionError, ValueError, RuntimeError with a 429 in the message — tells you which.
try:
risky_call()
except Exception as e:
print(f"caught: {type(e).__name__}: {e}")type(e).__name__ is short and stable. str(e) is the message; useful but variable. Log both.
| Class | Likely cause | Action |
|---|---|---|
KeyError | dict[key] missed; you assumed a field was there | use .get(key, default) |
IndexError | list[i] but list is shorter | guard with if items: |
TypeError | passing wrong type — len(None) | check upstream .get defaults |
ValueError | right type, bad value | validate inputs |
ConnectionError | network unreachable | retry with backoff |
TimeoutError | call exceeded deadline | retry with longer timeout |
PermissionError | auth failed | reconnect, don't retry |
RuntimeError (Composio) | tool said no — message has details | inspect str(e) |
Once you know what classes to expect, catch them by name:
try:
risky_call()
except ConnectionError:
retry()
except PermissionError:
abort_and_alert()
except Exception as e:
print(f"unexpected: {type(e).__name__}: {e}")
raiseThe specific handlers fire first; the catch-all is the fallback. Always have a catch-all Exception handler — surprise classes shouldn't crash silently.
For today's lesson, force three different classes to fire:
# KeyError
d = {}
try:
d["missing"]
except Exception as e:
print(type(e).__name__)
# ZeroDivisionError
try:
1 / 0
except Exception as e:
print(type(e).__name__)
# ValueError
try:
int("not a number")
except Exception as e:
print(type(e).__name__)Three errors deliberately raised; three class names printed. The shape generalizes to any try/except.
Create a free account to get started. Paid plans unlock all tracks.