67

I'm learning to use python. I just came across this article: http://nedbatchelder.com/blog/200711/rethrowing_exceptions_in_python.html It describes rethrowing exceptions in python, like this:

try:
    do_something_dangerous()
except:
    do_something_to_apologize()
    raise

Since you re-throw the exception, there should be an "outer catch-except" statement. But now, I was thinking, what if the do_something_to_apologize() inside the except throws an error. Which one will be caught in the outer "catch-except"? The one you rethrow or the one thrown by do_something_to_apologize() ? Or will the exception with the highest priotiry be caught first?

7
  • 1
    The one that happens first. Commented Jul 28, 2014 at 18:37
  • The one you were catching ? Commented Jul 28, 2014 at 18:38
  • 1
    This is exactly why (a) you want to write exception handlers that either can't raise, or can only raise in specific situations that you understand very well, and (b) you want to actually get the exception object, at least during early debugging, so you can, e.g., print('do_something_dangerous raised {!r}'.format(e)) at least level and see what's going on. Commented Jul 28, 2014 at 18:42
  • 1
    Also, notice that in Ned's "real" code he handles only except Exception, not just bare except:. Partly this is to allow him to capture the exception with except Exception, e: (although this is an old blog, so it uses old syntax; you want except Exception as e:), and partly to avoid catching things like KeyboardInterrupt, which you very rarely want to handle. (When you do, make it explicit with except BaseException:. That wasn't an option in older Python, but assuming you're writing for, e.g., 2.6 or 3.0, it is.) Commented Jul 28, 2014 at 18:45
  • 1
    @user2815780: For that case, you probably want to handle the apologize code in some way, so you can raise an exception that lets you code/the end user know that you failed and then failed to clean up. (Or maybe create the folder in a temporary location on the destination drive/filesystem so the worst-case scenario isn't as bad and maybe doesn't have to be reported.) But otherwise, yeah, that's a perfectly reasonable use to exception-handling code that may raises. Commented Jul 28, 2014 at 19:18

3 Answers 3

100

Try it and see:

def failure():
    raise ValueError, "Real error"

def apologize():
    raise TypeError, "Apology error"

try:
    failure()
except ValueError:
    apologize()
    raise

The result:

Traceback (most recent call last):
  File "<pyshell#14>", line 10, in <module>
    apologize()
  File "<pyshell#14>", line 5, in apologize
    raise TypeError, "Apology error"
TypeError: Apology error

The reason: the "real" error from the original function was already caught by the except. apologize raises a new error before the raise is reached. Therefore, the raise in the except clause is never executed, and only the apology's error propagates upward. If apologize raises an error, Python has no way of knowing that you were going to raise a different exception after apologize.

Note that in Python 3, the traceback will mention both exceptions, with a message explaining how the second one arose:

Traceback (most recent call last):
  File "./prog.py", line 9, in <module>
  File "./prog.py", line 2, in failure
ValueError: Real error

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "./prog.py", line 11, in <module>
  File "./prog.py", line 5, in apologize
TypeError: Apology error

However, the second exception (the "apology" exception) is still the only one that propagates outward and can be caught by a higher-level except clause. The original exception is mentioned in the traceback but is subsumed in the later one and can no longer be caught.

Sign up to request clarification or add additional context in comments.

2 Comments

is there a way to get the python 3 behaviour in python 2?
I personally find the python 3 behavior somewhat opinionated in a wrong way (better have let the catcher decide whether to log the exception they captured or not) and also not consistent with catching without re-raising. The python 3 text "During handling" showing between every two exceptions in the chain of exception captures is quite exacerbating on this tangent. The extended syntax from the other answer side-steps this dubious message, while still making you emit a stacktrace hierarchy of exceptions even if you are fine with only the final one that you choose to raise.
14

There are several methods to resolve it:

1. Exception Chain

raise NewException("Explain why") from CatchedException

pattern. In particular, considering Python 3 and the example given by @BrenBarn I use following

def failure():
    raise ValueError("Real error")

try:
    failure()
except ValueError as ex:
    raise TypeError("Apology error") from ex

which yields

--------- ValueError----                                
Traceback (most recent call last) 
      4 try:
----> 5     failure()
      6 except ValueError as ex:

      1 def failure():
----> 2     raise ValueError("Real error")
      3 

ValueError: Real error

The above exception was the direct cause of the following exception:

-----TypeError-----
Traceback (most recent call last) 
      5     failure()
      6 except ValueError as ex:
----> 7     raise TypeError("Apology error") from ex

TypeError: Apology error

2. Suppress Original Failure

You can also suppress the original exception context by raise the new exception from None. The code will change to

def failure():
    raise ValueError("Real error")

try:
    failure()
except ValueError as ex:
    raise TypeError("Apology error") from None

.. and it yields following output

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[2], line 7
      5     failure()
      6 except ValueError as ex:
----> 7     raise TypeError("Apology error") from None

TypeError: Apology error

3. Add Notes To Exception

You can also add explanation notes to original exception

def failure(i):
    raise ValueError(f"Real error in failure method.")

try:
    a = 3.1415
    failure(a)
except ValueError as ex:
    ex.add_note(f'The "{ex.args[0]}" happened with argument {a} when invoked from main script.')
    ex.add_note('Final note: bye')
    raise

which yields ...

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[14], line 6
      4 try:
      5     a = 3.1415
----> 6     failure(a)
      7 except ValueError as ex:
      8     ex.add_note(f'The "{ex.args[0]}" happened with argument {a} when invoked from main script.')

Cell In[14], line 2, in failure(i)
      1 def failure(i):
----> 2     raise ValueError(f"Real error in failure method.")

ValueError: Real error in failure method.
The "Real error in failure method." happened with argument 3.1415 when invoked from main script.
Final note: bye

3 Comments

As documented in the docs, and it's probably not elegantly possible to make only the final exception of the chain be raised and shown even if one wanted to (happy to learn otherwise).
@matanox For some reason the link you say points to the docs actually links to this answer. What did you have in mind?
@metanox I edited the answer to suppress "cause" exception and print just the final exception. However I am not sure if this is your desired "final exception in chain"
8

The exception thrown by do_something_to_apologize() will be caught. The line containing raise will never run, because of the exception thrown by do_something_to_apologize. Also, I don't believe there is any idea of "priority" in python exceptions.

1 Comment

Seems the other answer got the most votes, possibly due to its detail and "try it and see" approach (always good), but imho it doesn't actually answer the original question which was "Which one will be caught in the outer 'catch-except'? The one you rethrow or the one thrown by do_something_to_apologize() ?" This answer by ibebrett does answer that question. The general rule is that once something raises an exception, nothing else can also raise an exception until that first exception is caught: thus if apologize() raises an exception then the raise right after it will never get executed.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.