Coding Zen: Don’t Catch Exceptions 2016-05-12

Usually the right thing to do with exceptions is to not catch them. This may seem counter-intuitive, so I will try to describe why I think it is a sound idea.

Consider this piece of Python code:

try:
    work()
except:
    print "Unhandled exception"
    sys.exit(1)

The code above prints an error message and halts execution if an exception occurs. This is a pretty common pattern, after all it is usually not fun to have stack traces printed on the console, which is the default behaviour when an exception goes uncaught. For example, here is the stack trace Python prints for the exception thrown by pressing Ctrl+C:

^CTraceback (most recent call last):
  File "test.py", line 6, in <module>
    work()
  File "test.py", line 4, in work
    time.sleep(30)
KeyboardInterrupt

The stack trace looks scary, like something broke horribly. For that reason we don’t want to expose stack traces to users, so it’s common to avoid the stack trace by using the previous code pattern to catch all exceptions.

I will now try to show why the above pattern, and the idea of eagerly catching exceptions, is harmful.

Obscuring error causes

When an exception is caught the responsibility to inform the user of what went wrong is on the code that catches the exception. If the catching code prints a generic error message we won’t know what caused the error. The worst thing you can do is to not print anything at all – that will leave the user not knowing if anything went wrong and that leads to a more frustrating experience than if they clearly know something went wrong.

Even if the exception catching code prints an error, you have to include enough information to be able to debug the error later. If your code does a lot of stuff which may cause exceptions, how do you know which part caused the program to stop and print “Unhandled exception”?

Catching exceptions with too large nets causes problems by obscuring the cause of errors in the code. The code below is harder than necessary to debug:

try:
    work()
except Exception as e:
    print e

Even though we print the exception it may be difficult to track down the cause. The print statement only prints the exception message, not the location it was thrown. For example, if there is a divide-by-zero error somewhere in work(), or the code called by work(), then the above code prints out integer division or modulo by zero. That’s not very nice, especially when you get this in a bug report from a user. How will you track down the location? It was probably a bug that triggers seldom, under conditions that don’t happen in your test environment. It would be much more useful to get the stack trace which clearly includes the location of the error:

Traceback (most recent call last):
  File "test.py", line 8, in <module>
    work()
  File "test.py", line 4, in work
    1/0
ZeroDivisionError: integer division or modulo by zero

If your program will be run by non-technical users it might be a good idea to write the stack trace to a file which they can attach in a bug report. Catching exceptions so that they are logged with the stack trace is a fine approach, but the key idea here is that exceptions should be allowed to propagate up to a handler that does more than just say “something went wrong”.

Don’t catch all exceptions

In most cases, it is not worth catching exceptions. Rather than having to convince yourself that you should not catch an exception, you should convince yourself that you should catch it. This mindset is important even in languages that enforce stricter handling of exceptions, like Java:

private void work() throws IOException { ... }
void doSomething() { work(); }

Should the doSomething() method above declare IOException to be thrown, or should it handle the exception? The compiler forces you to pick one. I would argue that you should let the exception be thrown, and not catch it, unless you have a good reason to catch the exception.

Which suggested fix would you choose?

In Java, unfortunately, programmers are conditioned to handle the exception locally because adding a new exception to a method signature often means that we just have to make the same decision in potentially multiple other locations in the code. Thus, sloppy exception handling is common in Java. I’ve often seen code that just adds a catch clause to get rid of a compile error.

I have had to solve many bugs in Java code, and some of those bugs were needlessly obscured by over-eager exception catching. The problem is not exclusive to Java, but I think it is more common in Java than Python.

When to handle exceptions

There do exist good reasons to catch exceptions, I just mean that we should convince ourselves that we really need to catch the exception before we blindly add code to catch it. Writing code without thinking it through is pointless busywork, and often leads to stupid mistakes. I notice myself falling in this trap constantly.

One good reason to catch an exception is if the operation that caused the exception should be retried. This is only feasible for certain combinations of operations and exceptions, so in those cases only specific exceptions should be caught.

Two examples of operations that can/should be retried are:

  • Downloading a file when an network error occurs.
  • Asking a user for a password and it was incorrect.

Another good reason to catch an exception is if the exception is benign or if the operation was an optional step that is safe to skip. Again, it is important to make sure that only the specific exception to be suppressed is caught.

It is not a good idea to catch more exceptions than needed, and the worst case is to indiscriminately catch all exceptions. Catch only the ones that you know you want to handle!

Let’s go back to the first example with the Python code and for instance say that we want to not print the stack trace when the user hits Ctrl+C – the program should just silently exit. That’s fine, but keep in mind that it is important to ensure that we don’t hide serious errors by catching too many exceptions. The following code solves the problem by catching only KeyboardInterrupt exceptions while letting other exceptions pass through:

try:
    work()
except KeyboardInterrupt:
    print "Unhandled exception"
    sys.exit(1)

Providing additional info

It is sometimes useful to provide some additional context for an exception by catching it and then re-throwing it. For example:

try:
    work()
except:
    print "Something went wrong while doing the work"
    raise

This is fine, the exception handler may even provide a useful bit of contextual information that can help later debugging. I don’t really think of this as catching an exception though, since it is re-thrown at the end of the exception handling code.

The Zen

The final Zen of the matter with exceptions is that the default should be to not handle exceptions, and only catch the ones that there is a specific reason to catch.

Not catching exceptions can simplify your code. It removes a few lines for exception handlers, simplifies the control flow, and gives peace of mind that some bugs are not needlessly obscured.

Categories: Programming

Leave a Reply

Your email address will not be published. Required fields are marked *