Exception chaining solves two problems in Python 2.
1. Swallowed Exceptions
If, while handling exception A, exception B inadvertently occurred, exception A was completely lost.
# Python 2
try:
raise ValueError('original problem')
except Exception as exc:
4/0 # bad calculation while handling the original problem
Sadly, the original problem is nowhere to be found in our Python 2 stack trace:
Traceback (most recent call last):
File "/tmp/t1.py", line 4, in <module>
4/0
ZeroDivisionError: integer division or modulo by zero
2. Preserving Tracebacks
Sometimes it’s useful, when catching an exception, to raise a new exception (e.g., a custom error type). In Python 2, the original exception could only be preserved to the extent that you could explicitly include it in the new exception’s message or args. It’s annoying enough to have to encode the original exception’s message into your new exception; but if you want to pass along its stack trace, then you’re pretty much out of luck. (Unless you want to add several confusing lines to your except block; even then, you’d have to pre-format the traceback in a way that you knew would be appropriate for use downstream. This is not a good situation to find yourself in.)
# Python 2
try:
# some code that raises an exception
except Exception as exc:
# the traceback of "exc" will be lost once we raise the following exception
raise MyErrorType('The flurble operation failed due to {}'.format(exc))
Implicit Exception Chaining
Python implicitly chains any exception that occurs while handling another exception. This solves problem #1:
# Python 3 (but same code as Python 2 version above)
try:
raise ValueError('original problem')
except Exception as exc:
4/0 # bad calculation while handling the original problem
Without any modification to the code, note how Python 3 keeps track of the exception chain for us:
Traceback (most recent call last):
File "/tmp/t1.py", line 2, in <module>
raise ValueError()
ValueError
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/tmp/t1.py", line 4, in <module>
4/0
ZeroDivisionError: division by zero
Explicit Exception Chaining
If we explicitly state that some exception was the proximate cause of another, Python 3 will remember the ensuing causal chain for us:
# Python 3
try:
a = 3
b = 0
x = a / b # uh-oh
except Exception as exc:
raise ValueError('Some data was bad') from exc
(Note the “from exc” on the last line.)
The chain of exceptions is preserved and displayed, each with its own stack trace:
Traceback (most recent call last):
File "/tmp/t.py", line 20, in <module>
x = a / b # uh-oh
ZeroDivisionError: division by zero
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/tmp/t.py", line 23, in <module>
raise ValueError('Some data was bad') from exc
ValueError: Some data was bad
Happy chaining!