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.
1
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.
- 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
- Recover to the happy path: But you aren’t doing this as you don’t continue to the rest of the functions.
- Translates the exception type: But you aren’t throwing another exception
- 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.
6
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.
1
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()
4
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()