Raising & Chaining
Translate low-level failures into domain errors without losing context. Use raise ... from, bare raise, and from None intentionally so tracebacks tell the full story.
Recipe
def load_user(user_id: str) -> dict:
try:
return fetch_row(user_id)
except KeyError as exc:
raise UserNotFoundError(user_id) from excWhen to reach for this:
- Wrapping third-party exceptions at API boundaries
- Adding business context while preserving root cause
- Suppressing noisy intermediate tracebacks (
from None) - Logging then re-raising with bare
raise - Chaining validation errors in layered architectures
Working Example
class UserNotFoundError(LookupError):
def __init__(self, user_id: str) -> None:
self.user_id = user_id
super().__init__(f"user not found: {user_id}")
_DB: dict[str, dict] = {"1": {"name": "Ada"}}
def fetch_row(user_id: str) -> dict:
return _DB[user_id]
def get_user(user_id: str) -> dict:
try:
return fetch_row(user_id)
except KeyError as exc:
raise UserNotFoundError(user_id) from exc
def parse_id(raw: str) -> str:
try:
value = int(raw)
except ValueError:
raise ValueError(f"invalid id: {raw!r}") from None
if value < 1:
raise ValueError("id must be positive")
return str(value)
try:
get_user("999")
except UserNotFoundError as exc:
print(exc)
print(exc.__cause__)What this demonstrates:
from excsets__cause__for debugging chainsfrom Nonehides parser internals from API consumers- Domain exceptions carry identifiers for handlers and logs
- Bare validation raises still produce clear messages
Deep Dive
How It Works
raise NewError()starts a new exception at current traceback pointraise NewError() from exclinksexcas__cause__- Bare
raiseinsideexceptre-raises the caught exception in-flight raise excfrom stored variable can reset traceback context - prefer bareraiseException.__context__may differ from__cause__when implicit chaining occurs
Chaining Choices
| Syntax | Effect |
|---|---|
raise A from b | Explicit cause shown in traceback |
raise (bare) | Same exception continues |
raise A from None | Suppress displayed context |
Gotchas
- Losing cause with
raise New()withoutfrom- implicit context may still appear but is ambiguous. Fix: always usefrom excwhen wrapping. - Swallowing with
from Noneeverywhere - hides root causes in ops. Fix: reservefrom Nonefor user-input parsing boundaries. raise excafter except block ends - traceback points at reraise site. Fix: bareraiseonly inside activeexcept.- Chaining loops - A from B from A in recursive wrappers. Fix: raise domain error once at boundary.
- Logging
exc_infothen raising different type withoutfrom- logs and traceback disagree. Fix: chain or log inside handler that bare re-raises.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Result/Either types | Functional core, no exceptions | Python service boundaries expecting raises |
| Error codes | C extensions, protocols | Python application layers |
| Aggregate in ExceptionGroup | Parallel failures | Single failure path |
FAQs
When should I use from None?
When the underlying exception is an implementation detail for callers - typically input parsing at public API edges.
What is __context__ vs __cause__?
__cause__ is explicit from. __context__ is implicit link when raising during handling another exception.
Should I wrap all KeyError?
Only at module boundaries. Internal code can let KeyError propagate within a package.
How do loggers use exception info?
log.exception("msg") or exc_info=True captures the active exception traceback at log time.
Can I chain custom exceptions?
Yes. Subclass Exception, raise with from, and add fields on the class for structured handling.
Does finally interact with chaining?
finally runs before exception propagates; chaining set in except is preserved.
How do frameworks map exceptions?
FastAPI/Flask handlers map exception types to HTTP responses - chain to root cause for 500 logs.
Should I re-raise after retry exhaustion?
Yes - raise last_exc from last_exc or simply raise the final failure with context on attempt count.
What about assert?
assert becomes AssertionError - not for runtime validation in production; use explicit raises.
How do I test exception chains?
pytest.raises and assert exc.value.__cause__ is the expected type.
Related
- Custom Exception Hierarchies - domain types
- Exceptions Basics - try/except structure
- Retries & Backoff - raise after retries
- Errors & Logging Best Practices - operational rules
Stack versions: This page was written for Python 3.14.0 (stable 3.14, maintenance 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.