Python Exception Handling: Cleanup and Reraise

I've had this code around for a while and had an opportunity to drag it out the other day and dust it off. The problem: Every now and again there's a situation where you don't really want to catch an exception, but you do want to perform some cleanup and let the exception propagate up the stack. Sometimes there's an extra wrinkle in that the cleanup code may itself throw an exception (that I'm simply going to assume we can ignore).

You can run the file to see the behavior. Simply provide an integer 1-5 as a command line argument and you'll run the selected scenario and see the output. The goal in this case is for cleanup to occur and an exception to be reported as having occurred at line 10.

The code in reraiser1 is wrong because this behaves as if a brand new exception were thrown at line 27. That may not seem so bad, but this code is pretty simple. If this happens and the stack trace is deep, it will be almost impossible to diagnose what went wrong.

The code in reraiser2 shows what happens when a second exception occurs in the except block. A bare raise statement here might be an attempt to re-raise the original exception, but python's rules about re-raising specify that the most recent exception in the scope is what is reraised. In this case, that's the exception thrown from the cleanup function. Again, this makes troubleshooting difficult.

In reraiser3 I worked around the problem in reraiser2 by moving the cleanup function's exception into a separate scope by defining a local cleanup function and calling it from within the except block. This prevents the cleanup function's exception from polluting the scope with an irrelevant exception and we can re-raise the original exception. This results in a stack trace rooted at line 10.

Reraiser4 takes a different approach. Instead of moving the cleanup function's exception into a separate scope, it captures the traceback information from the original exception and then passes it back to the raise statement so that the reported traceback is accurate.

The cleanest way to handle this is to use a finally block as shown in reraiser5. This situation is what "finally" is meant for: it does not trap the exception, it just gives you a chance to clean up before control moves back up the stack to the caller. The presence of finally clues readers in to the fact that you aren't messing with the exception, and that the point of the block is to perform cleanup.

Kindly drop me a note if I've got something wrong above, or if I'm missing a technique (or a common anti-pattern!). Thanks.

Posted on 2009-07-15 by brian in python .

Comments

I guess the point of my comment is to affirm your thinking here: I hadn't done a lot of coding with exception handling before this past year, but for a project that I was working on (in Java) I eventually arrived at the same pattern that you did here: using "finally" is the way to go, in my opinion. It is refreshing to arrive at a clean code pattern for problems like this, in contrast to some of the messier solutions (or, in fact, buggy code...) that I have seen elsewhere...

kevin clark
2009-07-17 14:45:02

Haridara left a comment on my (now abandoned -- a short lived experiment) posterous version of this blog.

I am quoting here because it includes a helpful link and useful tips:

Also see http://blog.ianbicking.org/2007/09/12/re-raising-exceptions/ for a good discussion on the different scenarios. Regarding reraiser5(), See the specific discussion in the comments on this pattern. Couple of comments I have are: - You wouldn't want to just "pass", you should at least the exception, e.g., traceback.print_exc() - Sometimes you want to cleanup conditionally, only if the operation fails. - During cleanup, it is better to be more broader in catching exceptions. Here is an example that I posted at the other blog as well:
success = 0
try:
    do_db_operation()
    success = 1
finally:
    if not success:
        try:
            do_rollback()
        except (KeyboardInterrupt, SystemExit):
            raise
        except:
            logging.getLogger().exception("Exception in cleanup ignored due to a pending exception on stack")
    else:
        do_commit()
Brian St. Pierre
2009-11-10 03:32:46
Comments on this post are closed. If you have something to share, please send me email.