[Tutor] Try except really better than if?

Steven D'Aprano steve at pearwood.info
Mon Jan 10 11:58:56 CET 2011


Alan Gauld wrote:

> However, using exceptions like if/else blocks is bad practice.

I'm afraid I disagree strongly with that!

> You need to use them like a commit in SQL - to protect a whole
> block of code. Putting try/except around every line of code
> defeats the purpose, wrap atomic blocks of code not the
> individual lines.

Wrapping an atomic block of code, so long as it actually *can* be 
treated as atomic (including backing out any changes if it fails), is a 
good use of try...except. But otherwise, this is risky advice. Take your 
example:

> try:
>   something
>   something else
>   another
>   the last
> except Error1: # do something
> except Error2: # do something
> except Error3: # do something
> finally: # tidy up


Provided the entire try block needs to be treated as a single operation 
(in which case, why isn't it a function?) then that's not too bad. But 
the risk comes from people who wrap large blocks of code *without* 
making them atomic transactions:

try:
     get_job_id()
     query_database()
     send_results()
     push_ticket()
     update_counter()
except KeyError:
     print "job failed"


And now an error in push_ticket means that the counter fails to be 
updated even though the results were sent. Problems occur.

The try block must contain ONE operation. It doesn't matter if the 
operation is a simple, atomic operation:

try:
     y = x + 1
except...

or a large, complex transaction, so long as it can safely be treated as 
a single operation. That is, the individual sub-tasks should have no 
side-effects, or they must be backed out off on failure.

The other problem is that large try blocks can mask bugs. Here's my 
example again:

try:
     get_job_id()
     query_database()
     send_results()
     push_ticket()
     update_counter()
except KeyError:
     print "job failed"


The idea being that get_job_id or query_database might raise KeyError. 
But update_counter should never raise KeyError. If it does, that 
indicates a bug in update_counter, which will then be masked by the 
except as a normal error in processing. This would be better:

try:
     get_job_id()
     query_database()
except KeyError:
     print "job failed"
else:
     send_results()
     push_ticket()
     update_counter()


Now the else block is only executed if there is no KeyError, and any 
exceptions in those three will be detected and not masked.

Of course, you can then wrap the entire code block in another 
try...except, try...else or try...finally block. Something like this 
perhaps?


try:
     try:
         get_job_id()
         query_database()
     except KeyError:
         print "job failed"
     else:  # no exception caught
         send_results()
         push_ticket()
         update_counter()
         failed = True
finally:
     if transaction_succeeded():
         commit_transaction()
     else:
         rollback()


And now you are beginning to see why databases are so complicated.


> is better to maintain than
> 
> try: something
> except Error1: # do something
> try: something else
> except Error2: # do something
> try: another
> except Error1: # and again
> try: the last
> except Error3: do it
> tidy up


That's what you have to do if something, something else, etc. need to be 
treated as independent operations that have to be done regardless of the 
success or failure of those that come before. Otherwise:

try:
     something
     try:
         something_else
         try:
             another
         except Error1:  ...
     except Error2:  ...
except Error1: ...


It has been said that error handling is ten times harder than handling 
the rest of your code.



-- 
Steven



More information about the Tutor mailing list