How to re-raise an exception in nested try/except blocks?
Asked Answered
I

4

195

I know that if I want to re-raise an exception, I simple use raise without arguments in the respective except block. But given a nested expression like

try:
    something()
except SomeError as e:
    try:
        plan_B()
    except AlsoFailsError:
        raise e  # I'd like to raise the SomeError as if plan_B()
                 # didn't raise the AlsoFailsError

how can I re-raise the SomeError without breaking the stack trace? raise alone would in this case re-raise the more recent AlsoFailsError. Or how could I refactor my code to avoid this issue?

Ibbetson answered 12/8, 2013 at 13:42 Comment(7)
Have you tried putting plan_B in another function that returns True on success, and False on exception? Then the outer except block could just be if not try_plan_B(): raiseMarga
@DrewMcGowen Unfortunately the more realistic case is that this is inside a function accepting arbitrary objects arg and I'd try calling arg.plan_B() which might raise an AttributeError due to arg not providing a plan BIbbetson
Have a look at the traceback module: docs.python.org/2/library/traceback.html#traceback-examplesPayment
@Payment Thanks, I will (Though an answer already shows a simpler way)Ibbetson
@DrewMcGowen I wrote up an answer based on your comment, which looks less pythonic than user4815162342's answer though. But that's due to my wanting to also have a return value and allowing plan_B to raise exceptionsIbbetson
I'm confused by the code example. There is no raise w/o arguments. The code correcly reraises 1st exception. The comment is misleading: why reraise is in except AlsoFailsError block if the comment tells "as if plan_B() didn't raise the AlsoFailsError".Edema
@JCode Honestly I don't remember my exact thought from five years ago, the example looks exactly like the accepted answer's Python3 code... But as mentioned below I'd probably use raise from now anywayIbbetson
B
227

As of Python 3, the traceback is stored in the exception, so a simple raise e will do the (mostly) right thing:

try:
    something()
except SomeError as e:
    try:
        plan_B()
    except AlsoFailsError:
        raise e  # or raise e from None - see below

The traceback produced will include an additional notice that SomeError occurred while handling AlsoFailsError (because of raise e being inside except AlsoFailsError). This is misleading because what actually happened is the other way around - we encountered AlsoFailsError, and handled it, while trying to recover from SomeError. To obtain a traceback that doesn't include AlsoFailsError, replace raise e with raise e from None.


In Python 2 you'd store the exception type, value, and traceback in local variables and use the three-argument form of raise:

try:
    something()
except SomeError:
    t, v, tb = sys.exc_info()
    try:
        plan_B()
    except AlsoFailsError:
        raise t, v, tb
Breaux answered 12/8, 2013 at 13:47 Comment(15)
Perfect, that's what I just also found here, thanks! Though there the suggestion is raise self.exc_info[1], None, self.exc_info[2] after self.exc_info = sys.exc_info() - putting [1] to first position for some reasonIbbetson
@TobiasKienzler raise t, None, tb will lose the value of the exception and will force raise to re-instantiate it from the type, giving you a less specific (or simply incorrect) exception value. For example, if the raised exception is KeyError("some-key"), it will just re-raise KeyError() and omit the exact missing key from the traceback.Breaux
So your solution is better. In fact that other post seems to call raise v, None, tb, which should probably fail due to the exception's class missing. Hm, sys.exc_info() has a pretty red warning about assigning the traceback value to a local variable, does this require being taken care of in this case? edit Ah, there's a box below stating that starting from 2.2 that's handled automatically though it should still be avoided when possibleIbbetson
I hacked up another solution that doesn't require sys, but it looks less pythonic to me, so I accept your answer nonethelessIbbetson
Ah, note that as stated here, this won't work in Python 3 due to a different raise syntaxIbbetson
@TobiasKienzler It should still possible to express that in Python 3 as raise v.with_traceback(tb). (Your comment even says as much, except it proposes to re-instantiate the value.)Breaux
Also, the red warning not to store sys.exc_info() in a local variable made sense prior to Python 2.0 (released 13 years ago), but borders on ridiculous today. Modern Python would be near-useless without the cycle collector, as every non-trivial Python library creates cycles without pause and depends on their correct cleanup.Breaux
Thanks for clarifying, then that warning shouldn't be so prominent in 2.7's docs... Re the Python 3 issue, I misread the sys.exc_info() doc thinking the second return value were not the exception instance but the parameters passed to its __init__ m-/Ibbetson
get a TypeError: exceptions must be old-style classes or derived from BaseException, not NoneType for ` raise t, v, tb`Baca
@Baca Does that happen every time, or only occasionally? Can you print sys.exc_info()? Is there additional code between except SomeError and t, v, tb = sys.exc_info() lines?Breaux
@Breaux I found your comment WRT Python 3 (raise v.with_traceback(tb)) very useful. Would you consider adding it to your answer?Zachar
With Python3.5 the whole dance is not necessary anyway because the traceback is already stored in the exception. Thus except SomeError as v: and raise v is sufficient.Afterpiece
@MatthiasUrlichs You're right, I've now removed .with_traceback(tb) and the call to sys.exc_info(). Unfortunately Python 3 specifies both tracebacks with the "during handling of the above exception, another exception occurred" message, which is likely not wanted here. The amended answer includes the workaround for that as well.Breaux
@Breaux You can kill the "another error occurred" nested error by writing "raise e from None".Afterpiece
t, v, tb = sys.exc_info() raise t(v).with_traceback(tb) it works for python2/3 bothRincon
K
30

Even if the accepted solution is right, it's good to point to the Six library which has a Python 2+3 solution, using six.reraise.

six.reraise(exc_type, exc_value, exc_traceback=None)

Reraise an exception, possibly with a different traceback. [...]

So, you can write:

import six


try:
    something()
except SomeError:
    t, v, tb = sys.exc_info()
    try:
        plan_B()
    except AlsoFailsError:
        six.reraise(t, v, tb)
Karate answered 28/9, 2017 at 10:27 Comment(3)
Good point - speaking of Six you can also use six.raise_from if you want to include information that plan_B() also failed.Ibbetson
@TobiasKienzler: I think it's a different usage: with six.raise_from you create a new exception which is linked to a previous one, you don't re-raise, so the trace back is different.Karate
My point exactly - if you reraise you get the impression only something() threw SomeError, if you raise_from you also know that this caused plan_B() to be executed but throwing the AlsoFailsError. So it depends on the usecase. I think raise_from will make debugging easierIbbetson
I
21

As per Drew McGowen's suggestion, but taking care of a general case (where a return value s is present), here's an alternative to user4815162342's answer:

try:
    s = something()
except SomeError as e:
    def wrapped_plan_B():
        try:
            return False, plan_B()
        except:
            return True, None
    failed, s = wrapped_plan_B()
    if failed:
        raise
Ibbetson answered 12/8, 2013 at 14:10 Comment(5)
The nice thing about this approach is that it works unchanged in Python 2 and 3.Breaux
@Breaux Good point :) Though meanwhile in Python3 I'd consider raise from, so the stack trace would also let me se plan B failed. Which can be emulated in Python 2 by the way.Ibbetson
you pass some error as 'e' but then dont use it? Also this doesn't reraise the the specific exception.Himelman
@Himelman as e could be omitted, it's just a snippet that could be used in a more general way. But what do you mean be "specific exception"? raise alone re-raises the exception from something() currently handled in the except scope, which was the intentionIbbetson
FYI, a bare raisere-raises the exception that is currently being handled” by try-except.Biometry
A
10

Python 3.5+ attaches the traceback information to the error anyway, so it's no longer necessary to save it separately.

>>> def f():
...   try:
...     raise SyntaxError
...   except Exception as e:
...     err = e
...     try:
...       raise AttributeError
...     except Exception as e1:
...       raise err from None
>>> f()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 9, in f
  File "<stdin>", line 3, in f
SyntaxError: None
>>> 
Afterpiece answered 25/4, 2017 at 8:25 Comment(4)
The question is about another exception happening during the except. But you're right, when I replace err = e by, say, raise AttributeError, you get first the SyntaxError stack trace, followed by a During handling of the above exception, another exception occurred: and the AttributeError stack trace. Good to know, though unfortunately one cannot rely on 3.5+ being installed. PS: ff verstehen nicht-Deutsche vermutlich nicht ;)Ibbetson
OK, so I changed the example to raise another exception, which (as the original question asked for) gets ignored when I re-raise the first one.Afterpiece
@TobiasKienzler 3.5+ (which I changed it to) seems to be a globally recognized format. Was denkst du? ;)Protuberant
@Protuberant Agreed :)Ibbetson

© 2022 - 2024 — McMap. All rights reserved.