I often run into situations where an error occurs, an exception is caught, and some action is taken before the exception is re-raised so that higher levels of the program can deal with it. Exactly how the exception is re-raised can have a big impact on how much information is available to someone trying to debug a problem. I got this wrong as a newbie, and I’ve seen even experienced python programmers get tripped up by these subtleties so I thought I would try to post a clear example. Consider the following program:
def bar(): raise Exception("Oops!") def foo(): bar() def dinkum(): foo() def fair(): try: dinkum() except Exception as e: print "Something failed!" raise e fair()
As you can see I’ve created a few methods in order to have a stack trace to look at, and raised an exception in the last one to be called, foo(). That exception is caught in fair() and some information is printed before the exception is re-raised by the statement “raise e.” This syntax looks pretty reasonable, so let’s run it and see what happens:
mark@viking:~/workspace$ python raise_test1.py Something failed! Traceback (most recent call last): File "raise_test.py", line 21, in fair() File "raise_test.py", line 19, in fair raise e Exception: Oops!
Hmm. Looks like we’re missing some useful information. The exception was raised on line 6 but the stack trace begins on line 19. Lets change the program slightly and run it again:
def bar(): raise Exception("Oops!") # ... snip for brevity ... def fair(): try: dinkum() except Exception as e: print "Something failed!" raise fair()
The only change here is that now we’re simply using “raise” rather than re-raising the exception by name with “raise e.” The output is quite different:
mark@viking:~/workspace$ python raise_test2.py Something failed! Traceback (most recent call last): File "raise_test.py", line 21, in fair() File "raise_test.py", line 16, in fair dinkum() File "raise_test.py", line 12, in dinkum foo() File "raise_test.py", line 9, in foo bar() File "raise_test.py", line 6, in bar raise Exception("Oops!") Exception: Oops!
That’s more like it. Now we can see the whole stack trace and see that the error originated on line 6 in the call to bar(). This would be helpful stuff to have in a debugging situation. So what’s going on here?
In some languages and runtimes exception objects carry their own stack trace information, but in python the interpreter is responsible for capturing the state of the call stack when an exception occurs. To access this information a program can call the sys.exc_info() method, which returns a tuple with the last traceback in item 3. We can use this method to get a better look at the state of the call stack when we catch an exception:
import sys import traceback def bar(): raise Exception("Oops!") # ... snip for brevity ... def fair(): try: dinkum() except Exception as e: print "Something failed!" traceback.print_tb(sys.exc_info()[2]) raise try: fair() except Exception as e: print "It sure looks like it!" traceback.print_tb(sys.exc_info()[2])
The print_tb() method defined in the traceback module is a handy way to get a prettyprint of the traceback object. When run this code produces the following:
mark@viking:~/workspace$ python raise_test3.py Something failed! File "raise_test.py", line 15, in fair dinkum() File "raise_test.py", line 11, in dinkum foo() File "raise_test.py", line 8, in foo bar() File "raise_test.py", line 5, in bar raise Exception("Oops!") It sure looks like it! File "raise_test.py", line 21, in fair() File "raise_test.py", line 15, in fair dinkum() File "raise_test.py", line 11, in dinkum foo() File "raise_test.py", line 8, in foo bar() File "raise_test.py", line 5, in bar raise Exception("Oops!")
The interpreter creates a new traceback when the exception is initially raised on line 5. As the stack is unwound back to the first “except” block each call point (method name and line number) is recorded on the traceback. As you can see above when we first catch the exception there are four call points on the traceback. When we catch it a second time after using “raise” to re-raise it, a fifth call point has been added. The crucial thing is that “raise” did not prompt the interpreter to start a new stack trace. What happens when instead of “raise” we use “raise e” to re-raise the exception?
Something failed! File "raise_test.py", line 15, in fair dinkum() File "raise_test.py", line 11, in dinkum foo() File "raise_test.py", line 8, in foo bar() File "raise_test.py", line 5, in bar raise Exception("Oops!") It sure looks like it! File "raise_test.py", line 21, in fair() File "raise_test.py", line 19, in fair raise e
In this case the interpreter started a new stack trace when it saw the “raise e” statement. In fact this is the exact same behavior you’d get by just raising a whole new exception with the statement “raise Exception(“Sorry, no traceback for you”).”
So when would you actually want to use “raise e?” To the extent that ‘e’ is an exception that you’ve just caught, never. Doing so will always lose the traceback information from the previous frames on the stack, and obfuscate the point of origin of the original error. However, it can sometimes make sense to translate one exception into another. Something like the following isn’t an unknown pattern:
try: some_vulnerable_op() except LowLevelException as e: very_important_cleanup() raise HighLevelException("Low level stuff failed")
While this is a valid pattern I’m not a huge fan of it unless information specific to the inner exception is logged or otherwise made available. There are a number of ways you could accomplish this goal, but we can leave that for another time. For now, suffice it to say that exception handling complicates reasoning about the control flow and behavior of a program, so it makes sense to keep your exception handling patterns as simple as you can.