19

How should I "rethrow" an exception, that is, suppose:

  • I try something in my code, and unfortunately it fails.
  • I try some "clever" workaround, which happens to also fail this time

If I throw the exception from the (failing) workaround, it's going to be pretty darn confusing for the user, so I think it may be best to rethrow the original exception (?), with the descriptive traceback it comes with (about the actual problem)...

Note: the motivating example for this is when calling np.log(np.array(['1'], dtype=object)), where it tries a witty workaround and gives an AttributeError (it's "really" a TypeError).

One way I can think of is just to re-call the offending function, but this seems doged (for one thing theoretically the original function may exert some different behaviour the second time it's called):
Okay this is one awful example, but here goes...

def f():
    raise Exception("sparrow")

def g():
    raise Exception("coconut")

def a():
    f()

Suppose I did this:

try:
    a()
except:
    # attempt witty workaround
    g()
---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
<ipython-input-4-c76b7509b315> in <module>()
      3 except:
      4     # attempt witty workaround
----> 5     g()
      6

<ipython-input-2-e641f2f9a7dc> in g()
      4
      5 def g():
----> 6     raise Exception("coconut")
      7
      8

Exception: coconut

Well, the problem doesn't really lie with the coconut at all, but the sparrow:

try:
    a()
except:
    # attempt witty workaround
    try:
        g()
    except:
        # workaround failed, I want to rethrow the exception from calling a()
        a() # ideally don't want to call a() again
---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
<ipython-input-4-e641f2f9a7dc> in <module>()
     19     except:
     20         # workaround failed, I want to rethrow the exception from calling a()
---> 21         a()  # ideally don't want to call a() again

<ipython-input-3-e641f2f9a7dc> in a()
      8
      9 def a():
---> 10     f()
     11
     12

<ipython-input-1-e641f2f9a7dc> in f()
      1 def f():
----> 2     raise Exception("sparrow")
      3
      4
      5 def g():

Exception: sparrow

Is there a standard way to deal with this, or am I thinking about it completely wrong?

Andy Hayden
  • 359,921
  • 101
  • 625
  • 535
  • related http://stackoverflow.com/questions/6299756/python-reraise-recatch-exception – maazza Jun 08 '13 at 16:31
  • Have you tried the [traceback](http://docs.python.org/2/library/traceback.html) module? – kirbyfan64sos Jun 08 '13 at 17:22
  • @kirbyfan64sos care to put an answer together using it? – Andy Hayden Jun 09 '13 at 11:31
  • Your intuition that the original exception should be re-raised is correct: that's what's done in Java try-with-resources (equivalent of Python `with` statements), but Java also adds the secondary exception as a "suppressed" exception, via `Throwable#addSuppressed`, so you actually can get a *tree* of exceptions! See [Who decides what exceptions get suppressed?](http://stackoverflow.com/q/11603300/2025416). – Nils von Barth Aug 31 '15 at 03:57

7 Answers7

10

If you want to make it appear to the end user that you never called g(), then you need to store the traceback from the first error, call the second function and then throw the original with the original traceback. (otherwise, in Python2, bare raise re-raises the second exception rather than the first). The problem is that there is no 2/3 compatible way to raise with traceback, so you have to wrap the Python 2 version in an exec statement (since it's a SyntaxError in Python 3).

Here's a function that lets you do that (I added this to the pandas codebase recently):

import sys
if sys.version_info[0] >= 3:
    def raise_with_traceback(exc, traceback=Ellipsis):
        if traceback == Ellipsis:
            _, _, traceback = sys.exc_info()
        raise exc.with_traceback(traceback)
else:
    # this version of raise is a syntax error in Python 3
    exec("""
def raise_with_traceback(exc, traceback=Ellipsis):
    if traceback == Ellipsis:
        _, _, traceback = sys.exc_info()
    raise exc, None, traceback
""")

raise_with_traceback.__doc__ = (
"""Raise exception with existing traceback.
If traceback is not passed, uses sys.exc_info() to get traceback."""
)

And then you can use it like this (I also changed the Exception types for clarity).

def f():
    raise TypeError("sparrow")

def g():
    raise ValueError("coconut")

def a():
    f()

try:
    a()
except TypeError as e:
    import sys
    # save the traceback from the original exception
    _, _, tb = sys.exc_info()
    try:
        # attempt witty workaround
        g()
    except:
        raise_with_traceback(e, tb)

And in Python 2, you only see a() and f():

Traceback (most recent call last):
  File "test.py", line 40, in <module>
    raise_with_traceback(e, tb)
  File "test.py", line 31, in <module>
    a()
  File "test.py", line 28, in a
    f()
  File "test.py", line 22, in f
    raise TypeError("sparrow")
TypeError: sparrow

But in Python 3, it still notes there was an additional exception too, because you are raising within its except clause [which flips the order of the errors and makes it much more confusing for the user]:

Traceback (most recent call last):
  File "test.py", line 38, in <module>
    g()
  File "test.py", line 25, in g
    raise ValueError("coconut")
ValueError: coconut

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "test.py", line 40, in <module>
    raise_with_traceback(e, tb)
  File "test.py", line 6, in raise_with_traceback
    raise exc.with_traceback(traceback)
  File "test.py", line 31, in <module>
    a()
  File "test.py", line 28, in a
    f()
  File "test.py", line 22, in f
    raise TypeError("sparrow")
TypeError: sparrow

If you absolutely want it to look like the g() Exception never happened in both Python 2 and Python 3, you need to check that you are out of the except clause first:

try:
    a()
except TypeError as e:
    import sys
    # save the traceback from the original exception
    _, _, tb = sys.exc_info()
    handled = False
    try:
        # attempt witty workaround
        g()
        handled = True
    except:
        pass
    if not handled:
        raise_with_traceback(e, tb)

Which gets you the following traceback in Python 2:

Traceback (most recent call last):
  File "test.py", line 56, in <module>
    raise_with_traceback(e, tb)
  File "test.py", line 43, in <module>
    a()
  File "test.py", line 28, in a
    f()
  File "test.py", line 22, in f
    raise TypeError("sparrow")
TypeError: sparrow

And this traceback in Python 3:

Traceback (most recent call last):
  File "test.py", line 56, in <module>
    raise_with_traceback(e, tb)
  File "test.py", line 6, in raise_with_traceback
    raise exc.with_traceback(traceback)
  File "test.py", line 43, in <module>
    a()
  File "test.py", line 28, in a
    f()
  File "test.py", line 22, in f
    raise TypeError("sparrow")
TypeError: sparrow

It does add an additional non-useful line of traceback that shows the raise exc.with_traceback(traceback) to the user, but it is relatively clean.

Jeff Tratner
  • 16,270
  • 4
  • 47
  • 67
  • 1
    Maybe at the time of writing this answer there was 'no 2/3 compatible way to raise with traceback' but now in [six](https://pythonhosted.org/six/) there is a function six.reraise that does pretty much what @jeff shows here. – agomcas Dec 19 '16 at 14:53
  • you're right! I didn't know that was in six (and it looks like it was actually added to the library in 2010!). It's implemented in a pretty similar way too – Jeff Tratner Dec 19 '16 at 18:53
8

Here is something totally nutty that I wasn't sure would work, but it works in both python 2 and 3. (It does however, require the exception to be encapsulated into a function...)

def f():
    print ("Fail!")
    raise Exception("sparrow")
def g():
    print ("Workaround fail.")
    raise Exception("coconut")
def a():
    f()

def tryhard():
    ok = False
    try:
        a()
        ok = True
    finally:
        if not ok:
            try:
                g()
                return # "cancels" sparrow Exception by returning from finally
            except:
                pass

>>> tryhard()
Fail!
Workaround fail.
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in tryhard
  File "<stdin>", line 2, in a
  File "<stdin>", line 3, in f
Exception: sparrow

Which is the correct exception and the right stack trace, and with no hackery.

>>> def g(): print "Worked around." # workaround is successful in this case

>>> tryhard()
Fail!
Worked around.

>>> def f(): print "Success!" # normal method works

>>> tryhard()
Success!
Andy Hayden
  • 359,921
  • 101
  • 625
  • 535
morningstar
  • 8,952
  • 6
  • 31
  • 42
  • If you don't want to encapsulate it in a function, the concept also works with `break` from a finally, although I was surprised to learn that `continue` in a finally is actually a syntax error! – morningstar Jun 08 '13 at 18:31
  • Um, I get `SyntaxError: 'break' outside loop` when trying it outside a function? (both py2 and 3) – Andy Hayden Jun 08 '13 at 18:45
  • Yes, you need to enclose the whole outer try block in some kind of gratuitous context like `for _ in range(1):`. I didn't say it was pretty. – morningstar Jun 08 '13 at 19:16
  • 1
    @morningstar You call it nutty that you used `finally` as it was meant to be... and which I totally overlooked. +1 – jpaugh Jun 09 '13 at 11:27
  • Yup, the `finally` clause is exactly the solution to this question: “When an exception has occurred in the `try` clause and has not been handled by an `except` clause (or it has occurred in a `except` or `else` clause), it is re-raised after the `finally` clause has been executed.” Tutorial: [Defining Clean-up Actions](https://docs.python.org/2/reference/compound_stmts.html#the-try-statement), Reference: [The try statement](https://docs.python.org/2/tutorial/errors.html#defining-clean-up-actions): – Nils von Barth Aug 31 '15 at 03:16
6

Ian Bicking has a nice primer on re-raising.

As a corollary, my rule is to only catch Exceptions that the code knows how to deal with. Very few methods actually hit this rule. For example, if I'm reading a file and an IOException is thrown, there is very little that method could reasonably do.

As a corollary to that, catching exceptions in "main" is reasonable if you can return to a good state and you don't just want to dump the user out; this only obtains in interactive programs.

The relevant section from the primer being the update:

try:
    a()
except:
    exc_info = sys.exc_info()
    try:
        g()
    except:
        # If this happens, it clobbers exc_info,
        # which is why we had to save it above
        import traceback
        print >> sys.stderr, "Error in revert_stuff():"
        # py3 print("Error in revert_stuff():", file=sys.stderr)
        traceback.print_exc()
    raise exc_info[0], exc_info[1], exc_info[2]

In python 3, the final raise could be written as:

ei = exc_info[1]
ei.filname = exc_info[0]
ei.__traceback__ = exc_info[2]
raise ei
Community
  • 1
  • 1
msw
  • 42,753
  • 9
  • 87
  • 112
  • It does seem like a bit of a hack... but this is the *only* working answer so far :) – Andy Hayden Jun 08 '13 at 16:44
  • Ian used to be a bit of a fixture around here; he's certainly spent his time in the trenches. I have no idea why his SO rep has crashed. – msw Jun 08 '13 at 16:47
  • This method (using the saved `exc_info()`) is about the only way to make things work after you've clobbered the previous exception info. (see also http://stackoverflow.com/questions/8760267/re-raise-python-exception-and-preserve-stack-trace) It needs modification for Python 3.x: http://stackoverflow.com/questions/15838224/python3-re-raising-an-exception-with-custom-attribute – torek Jun 08 '13 at 17:11
  • @torek I've updated answer to include that too for convenience. – Andy Hayden Jun 08 '13 at 17:26
  • @torek, Python 3 doesn't need to save exc_info. Just `raise`. See my answer. – Mark Tolonen Jun 08 '13 at 17:30
4

In Python 3 (specifically tested on 3.3.2), this all works better, there's no need for saving sys.exc_info. Don't re-raise the original exception within the second exception handler. Just note that the 2nd attempt failed and raise the original in the scope of the original handler, like so:

#!python3

try:
    a()
except Exception:
    g_failed = False
    try:
        g()
    except Exception:
        g_failed = True
    raise

Python 3 output correctly raising "sparrow" and showing traceback through a() and f():

Traceback (most recent call last):
  File "x3.py", line 13, in <module>
    a()
  File "x3.py", line 10, in a
    f()
  File "x3.py", line 4, in f
    raise Exception("sparrow")
Exception: sparrow

However, the same script on Python 2 incorrectly raising "coconut" and only showing g():

Traceback (most recent call last):
  File "x3.py", line 17, in <module>
    g()
  File "x3.py", line 7, in g
    raise Exception("coconut")
Exception: coconut

Here are the modifications to make Python 2 work correctly:

#!python2
import sys

try:
    a()
except Exception:
    exc = sys.exc_info()
    try:
        g()
    except Exception:
        raise exc[0], exc[1], exc[2] # Note doesn't care that it is nested.

Now Python 2 correctly shows "sparrow" and both a() and f() traceback:

Traceback (most recent call last):
  File "x2.py", line 14, in <module>
    a()
  File "x2.py", line 11, in a
    f()
  File "x2.py", line 5, in f
    raise Exception("sparrow")
Exception: sparrow
Andy Hayden
  • 359,921
  • 101
  • 625
  • 535
Mark Tolonen
  • 166,664
  • 26
  • 169
  • 251
3

Capture the error in your except clause, then manually re-raise it later. Capture the traceback, and reprint it via the traceback module.

import sys
import traceback

def f():
    raise Exception("sparrow")

def g():
    raise Exception("coconut")

def a():
    f()

try:
    print "trying a"
    a()
except Exception as e:
    print sys.exc_info()
    (_,_,tb) = sys.exc_info()
    print "trying g"
    try:
        g()
    except:
        print "\n".join(traceback.format_tb(tb))
        raise e
jpaugh
  • 6,634
  • 4
  • 38
  • 90
  • This still raises a coconut... I don't want to reraise the last exception, but the one before that. ? – Andy Hayden Jun 08 '13 at 16:30
  • 1
    Sorry, I skimmed. I don't know how to do that. – jpaugh Jun 08 '13 at 16:32
  • Perhaps you could save the exception (via `except`) and re-raise it directly, then? – jpaugh Jun 08 '13 at 16:35
  • Here, I rewrote my answer. – jpaugh Jun 08 '13 at 16:45
  • That only shows the last line of the exception, i.e. you lose the traceback, and that points to `raise e`. :( – Andy Hayden Jun 08 '13 at 16:45
  • Hmm. `sys.exc_info()` will give you a traceback object inside the `except` clause. I think you'd need to print it out yourself. Doable, but I've not worked with them before. – jpaugh Jun 08 '13 at 16:54
  • OOH, maybe you could replace the info in the second `except` clause, then let Python find the "new" one? – jpaugh Jun 08 '13 at 16:55
  • Hmm @msw uses `sys.exc_info()`... was hoping for a simple way really! :) – Andy Hayden Jun 08 '13 at 17:06
  • Ooh! There's a new `traceback` module since the last time I played with Python. This time I got really close. Try it. – jpaugh Jun 08 '13 at 17:14
  • Still the same issue with where it raises, and that it only shows one thing in the traceback. Surely traceback is the right module... :( – Andy Hayden Jun 08 '13 at 17:28
  • @jpaugh, you just needed to use [raise with 3 expressions](http://docs.python.org/2/reference/simple_stmts.html#the-raise-statement) from [Ian's primer via @msw's answer](http://stackoverflow.com/a/17001551/1020470). Also you can use the [`sys.last_traceback`](http://docs.python.org/2/library/sys.html#sys.last_traceback) attribute instead of dumping the 1st and 2nd outputs of `sys.exc_info()` and use [`traceback.print_tb(tb)`](http://docs.python.org/2/library/sys.html#sys.last_traceback) instead of `traceback.format_tb()` but you don't need `traceback` module at all. – Mark Mikofski Sep 11 '13 at 23:09
0

In Python 3, within a function, this can be done in a very slick way, following up the answer from @Mark Tolonen, who uses a boolean. You can't do this outside a function because there's no way to break out of the outer try statement: the function is needed for return.

#!python3

def f():
    raise Exception("sparrow")

def g():
    raise Exception("coconut")

def a():
    f()

def h():
    try:
        a()
    except:
        try:
            g()
            return  # Workaround succeeded!
        except:
            pass  # Oh well, that didn't work.
        raise  # Re-raises *first* exception.

h()

This results in:

Traceback (most recent call last):
  File "uc.py", line 23, in <module>
    h()
  File "uc.py", line 14, in h
    a()
  File "uc.py", line 10, in a
    f()
  File "uc.py", line 4, in f
    raise Exception("sparrow")
Exception: sparrow

...and if instead g succeeds:

def g(): pass

...then it doesn't raise an exception.

Nils von Barth
  • 3,239
  • 2
  • 26
  • 27
-1
try:
    1/0  # will raise ZeroDivisionError
except Exception as first:
    try:
        x/1  # will raise NameError
    except Exception as second:
        raise first  # will re-raise ZeroDivisionError
Jace Browning
  • 11,699
  • 10
  • 66
  • 90
  • 1
    Then I lose the rest of the traceback (it only gives back the last line, raise first). – Andy Hayden Jun 08 '13 at 16:34
  • 1
    I know this is demo code, but you never want to catch Exception as it could be a MemoryError which is quite difficult to cope with or `RuntimeError` which mostly means the interpreter is dying and anything you do is likely wrong. – msw Jun 08 '13 at 16:40
  • @msw that's my "mistake" :) – Andy Hayden Jun 08 '13 at 16:47