11

When writing code, I often want to do something like this:

try:
 foo()
except FooError:
 handle_foo()
else:
 try:
 bar()
 except BarError:
 handle_bar()
 else:
 try:
 baz()
 except BazError:
 handle_baz()
 else:
 qux()
finally:
 cleanup()

Obviously, this is completely unreadable. But it's expressing a relatively simple idea: execute a series of functions (or short code snippets), with an exception handler for each one, and stop as soon as a function fails. I imagine Python could provide syntactic sugar for this code, perhaps something like this:

# NB: This is *not* valid Python
try:
 foo()
except FooError:
 handle_foo()
 # GOTO finally block
else try:
 bar()
except BarError:
 handle_bar()
 # ditto
else try:
 baz()
except BazError:
 handle_baz()
 # ditto
else:
 qux()
finally:
 cleanup()

If no exceptions are raised, this is equivalent to foo();bar();baz();qux();cleanup(). If exceptions are raised, they're handled by the appropriate exception handler (if any) and we skip to cleanup(). In particular, if bar() raises a FooError or BazError, the exception will not be caught and will propagate to the caller. This is desirable so we only catch exceptions we're truly expecting to handle.

Regardless of syntactic ugliness, is this kind of code just a bad idea in general? If so, how would you refactor it? I imagine context managers could be used to absorb some of the complexity, but I don't really understand how that would work in the general case.

asked Oct 16, 2014 at 22:42
1
  • What kind of things are you doing in the handle_* functions? Commented Oct 17, 2014 at 1:42

4 Answers 4

9
try:
 foo()
except FooError:
 handle_foo()
else:
 ...
finally:
 cleanup()

What does handle_foo do? There are a few things we typically do in exception handling blocks.

  1. Cleanup after the error: But in this case, foo() should cleanup after itself, not leave us to do so. Additionally, most cleanup jobs are best handled with with
  2. Recover to the happy path: But you aren't doing this as you don't continue to the rest of the functions.
  3. Translates the exception type: But you aren't throwing another exception
  4. Log the error: But there shouldn't be any need to have special exception blocks for each type.

It seems to me that you are doing something odd in your exception handling. Your question here is simple a symptom on using exceptions in an unusual way. You're not falling into the typical pattern, and that's why this has become awkward.

Without a better idea of what you're doing in those handle_ functions that's about all I can say.

answered Oct 17, 2014 at 3:49
6
  • This is just sample code. The handle_ functions could just as easily be short snippets of code which (say) log the error and return fallback values, or raise new exceptions, or do any number of other things. Meanwhile, the foo(), bar(), etc. functions could also be short snippets of code. For instance, we might have spam[eggs], and need to catch KeyError. Commented Oct 17, 2014 at 14:01
  • 1
    @Kevin, right, but that doesn't change my answer. If your handler returned or raised anything, the problem you mention wouldn't arise. The rest of the function would be skipped automatically. In fact, a simple way to resolve your problem would be to return in all the exception blocks. The code is awkward because you aren't doing a customary bail or raise to indicate that you are giving up. Commented Oct 17, 2014 at 14:38
  • A good answer overall, but I have to disagree with your point 1 -- what evidence is there that foo should be doing its own cleanup? foo is signalling that something went wrong, and has no knowledge of what the correct cleanup procedure should be. Commented Oct 17, 2014 at 16:19
  • @EthanFurman, I think we may be thinking of different items under the heading of cleanup. I figure if foo opens files, database connections, allocates memory, etc, then it is foo's responsibility to ensure those are closed/deallocated before returning. That's what I meant by cleanup. Commented Oct 18, 2014 at 1:25
  • Ah, in that case I completely agree with you. :) Commented Oct 18, 2014 at 1:47
3

There are a couple different ways, depending on what you need.

Here's a way with loops:

try:
 for func, error, err_handler in (
 (foo, FooError, handle_foo),
 (bar, BarError, handle_bar),
 (baz, BazError, handle_baz),
 ):
 try:
 func()
 except error:
 err_handler()
 break
finally:
 cleanup()

Here's a way with an exit after the error_handler:

def some_func():
 try:
 try:
 foo()
 except FooError:
 handle_foo()
 return
 try:
 bar()
 except BarError:
 handle_bar()
 return
 try:
 baz()
 except BazError:
 handle_baz()
 return
 else:
 qux()
 finally:
 cleanup()

Personally I think the loop version is easier to read.

answered Oct 16, 2014 at 23:08
1
  • IMHO this is a better solution than the accepted one as it 1) attempts an answer, and 2) makes the sequence very clear. Commented Jan 6, 2020 at 22:24
2

It seems like you have sequence of commands that may throw an exception that needs to be handled before returning. Try grouping your code and and exception handling in separate locations. I believe this does what you intend.

try:
 foo()
 bar()
 baz()
 qux()
except FooError:
 handle_foo()
except BarError:
 handle_bar()
except BazError:
 handle_baz()
finally:
 cleanup()
answered Oct 17, 2014 at 1:47
4
  • 3
    I've been taught to try to avoid putting more code than absolutely necessary in the try: block. In this case, I would be concerned about bar() raising a FooError, which would be incorrectly handled with this code. Commented Oct 17, 2014 at 14:03
  • 3
    This may be reasonable if all the methods raise very specific exceptions, but that's generally not true. I'd heavily recommend against this approach. Commented Oct 17, 2014 at 14:40
  • @Kevin Wrapping each line of code in a Try Catch Else block quickly becomes unreadable. This is made more so by Python's indentation rules. Think how far this would end up indented if the code was 10 lines long. Would it be readable or maintainable. As long as the code follows the single responsibility I would stand by the example above. If not, then the code should be rewritten. Commented Oct 17, 2014 at 23:28
  • @WinstonEwert In a real life example I would expect some overlap in the Errors (exceptions). However, I would also expect the handling to be the same. BazError could be a keyboard interrupt. I would expect it to be handled appropriately for all four lines of code. Commented Oct 17, 2014 at 23:30
0

First of all, appropriate use of with can often reduce or even eliminate a lot of the exception handling code, improving both maintainability and readability.

Now, you can reduce the nesting in many ways; other posters have already provided a few, so here's my own variation:

for _ in range(1):
 try:
 foo()
 except FooError:
 handle_foo()
 break
 try:
 bar()
 except BarError:
 handle_bar()
 break
 try:
 baz()
 except BazError:
 handle_baz()
 break
 qux()
cleanup()
answered Oct 17, 2014 at 3:14

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.