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.
4 Answers 4
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.
-
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, thefoo()
,bar()
, etc. functions could also be short snippets of code. For instance, we might havespam[eggs]
, and need to catchKeyError
.Kevin– Kevin2014年10月17日 14:01:45 +00:00Commented Oct 17, 2014 at 14:01 -
1@Kevin, right, but that doesn't change my answer. If your handler
return
ed orraise
d 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– Winston Ewert2014年10月17日 14:38:17 +00:00Commented 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.Ethan Furman– Ethan Furman2014年10月17日 16:19:05 +00:00Commented 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.Winston Ewert– Winston Ewert2014年10月18日 01:25:21 +00:00Commented Oct 18, 2014 at 1:25
-
Ah, in that case I completely agree with you. :)Ethan Furman– Ethan Furman2014年10月18日 01:47:08 +00:00Commented Oct 18, 2014 at 1:47
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.
-
IMHO this is a better solution than the accepted one as it 1) attempts an answer, and 2) makes the sequence very clear.bob– bob2020年01月06日 22:24:38 +00:00Commented Jan 6, 2020 at 22:24
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()
-
3I've been taught to try to avoid putting more code than absolutely necessary in the
try:
block. In this case, I would be concerned aboutbar()
raising aFooError
, which would be incorrectly handled with this code.Kevin– Kevin2014年10月17日 14:03:15 +00:00Commented Oct 17, 2014 at 14:03 -
3This 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– Winston Ewert2014年10月17日 14:40:17 +00:00Commented 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.BillThor– BillThor2014年10月17日 23:28:17 +00:00Commented 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.BillThor– BillThor2014年10月17日 23:30:45 +00:00Commented Oct 17, 2014 at 23:30
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()
handle_*
functions?