8

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.

  • What kind of things are you doing in the handle_* functions? – Winston Ewert Oct 17 '14 at 1:42
8
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.

  • 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. – Kevin Oct 17 '14 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. – Winston Ewert Oct 17 '14 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. – Ethan Furman Oct 17 '14 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. – Winston Ewert Oct 18 '14 at 1:25
  • Ah, in that case I completely agree with you. :) – Ethan Furman Oct 18 '14 at 1:47
3

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()
  • 2
    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. – Kevin Oct 17 '14 at 14:03
  • 2
    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. – Winston Ewert Oct 17 '14 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. – BillThor Oct 17 '14 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. – BillThor Oct 17 '14 at 23:30
0

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.

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()

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.