Exception chaining: raise X from Y, __cause__ vs. implicit __context__
Raising a new exception while handling another one doesn't lose the original -- Python tracks it
automatically. raise NewErr() from original sets __cause__ explicitly (an intentional "this failure was
caused by that one" relationship). Just raising inside an except block without from still preserves the
original, but as __context__ (an incidental "this happened while handling that one" relationship) --
both show up in the traceback, worded differently.
def inner():
raise ValueError("original problem")
try:
try:
inner()
except ValueError as e:
raise RuntimeError("wrapped problem") from e
except RuntimeError as e:
print("explicit chain -- __cause__:", e.__cause__)
print("explicit chain -- __suppress_context__:", e.__suppress_context__)
try:
try:
inner()
except ValueError as e:
raise RuntimeError("wrapped without `from`") # still chained, just implicitly
except RuntimeError as e:
print("implicit chain -- __cause__:", e.__cause__)
print("implicit chain -- __context__:", e.__context__)
raise ... from None explicitly suppresses the chain display -- useful when the original exception is just noise (e.g. re-raising a cleaner, user-facing error):
try:
try:
inner()
except ValueError as e:
raise RuntimeError("clean, user-facing message") from None
except RuntimeError as e:
print("suppressed chain -- __cause__:", e.__cause__, "| __suppress_context__:", e.__suppress_context__)