Re-throwing exceptions in Python (original) (raw)

Tuesday 13 November 2007 — This is over 17 years old. Be careful.

When dealing seriously with error handling, an important technique is to be able to manipulate exceptions in ways other than simply throwing and catching them. One of these is to re-throw exceptions.

The simplest way to do this is if you need to perform a little work after the catch, but then immediately re-throw. This can be done with a simple raise statement:

try:
   do_something_dangerous()
except:
   do_something_to_apologize()
   raise

Here the raise statement means, “throw the exception last caught”. This is a simple case, and I probably didn’t need to remind you of it. But a more sophisticated technique is to catch an exception in one place, and raise it again in another.

For example, you may have a worker thread pre-fetching data from slow storage, and then on the main thread, the consumer of the data either gets the data or sees the exception that prevented him from getting the data.

Here’s the simple implementation:

1class DelayedResult:
2    def init(self):
3        self.e = None
4        self.result = None
5        
6    def do_work(self):
7        try:
8            self.result = self.do_something_dangerous()
9        except Exception, e:
10            self.e = e
11
12    def do_something_dangerous(self):
13        raise Exception("Boo!")
14
15    def get_result(self):
16        if self.e:
17            raise self.e
18        return self.result
19    
20dr = DelayedResult()
21dr.do_work()
22dr.get_result()

We store an exception in the object, and when retrieving the result, if there’s an exception, we raise it. It works:

$ python delayed.py
Traceback (most recent call last):
 File "C:\lab\delayed.py", line 22, in ?
   dr.get_result()
 File "C:\lab\delayed.py", line 17, in get_result
   raise self.e
Exception: Boo!

The only problem is, the traceback for the exception shows the problem starting in get_result. When debugging problems, it’s enormously helpful to know their real origin.

To solve that problem, we’ll store more than the exception, we’ll also store the traceback at the time of the original problem, and in get_results, we’ll use the full three-argument form of the raise statement to use the original traceback:

1class DelayedResult:
2    def init(self):
3        self.exc_info = None
4        self.result = None
5        
6    def do_work(self):
7        try:
8            self.result = self.do_something_dangerous()
9        except Exception, e:
10            import sys
11            self.exc_info = sys.exc_info()
12    
13    def do_something_dangerous(self):
14        raise Exception("Boo!")
15
16    def get_result(self):
17        if self.exc_info:
18            raise self.exc_info[1], None, self.exc_info[2]
19        return self.result
20    
21dr = DelayedResult()
22dr.do_work()
23dr.get_result()

Now when we run it, the traceback points to do_something_dangerous, called from do_work, as the real culprit:

$ python delayed.py
Traceback (most recent call last):
 File "C:\lab\delayed.py", line 23, in ?
   dr.get_result()
 File "C:\lab\delayed.py", line 8, in do_work
   self.result = self.do_something_dangerous()
 File "C:\lab\delayed.py", line 14, in do_something_dangerous
   raise Exception("Boo!")
Exception: Boo!

The three-argument raise statement is a little odd, owing to its heritage from the old days of Python when exceptions could be things other than instances of subclasses of Exception. This accounts for the odd tuple-dance we do on the saved exc_info.

It’s easy to write code that does the right thing when everything is going well. It’s much harder to write code that does a good job when things go wrong. Properly manipulating exceptions helps.