This article is part number 14 of the Readability series.


Try/catch blocks (or try/except, or whatever they happen to be named in your favorite language) are the mechanism by which you capture exceptions raised by a chunk of code in a controlled manner. Within that chunk of code, it does not matter which line throws the exception: any exception specified in the catch statement will be captured, and it is “impossible” to know at that point where it originated.

Consider this piece of code:

try:
    input_data = None
    with open("list.txt", "r") as input_file:
        input_data = load_data_from_file(input_file)

    connection = Database.connect()

    data_for_import = prepare_data_for_import(input_data)

    for entry in data_for_input:
        connection.add(entry)
except DatabaseError, e:
    logging.error('Failed to connect to the database')
    raise e
except IOError, e:
    logging.error("Failed to open the input file')
    raise e

Can you tell if there is anything wrong in this code? Maybe, maybe not, because there is not enough context to tell with certainty.

Let’s look at the exception handler for DatabaseError: if you check the error message, the error says "Failed to connect to the database". Is that right? The chunk of code protected by the try block has two calls to the database code: one via Database.connect() and another via connection.add(entry). So, which is it: a) is the message bogus because it applies to exceptions that arise when connecting and when writing to the database, b) does the write to the database raise a different exception that we are not handling, or c) does the call to connection.add(entry) not raise any exception (unlikely) and therefore our try block is too wide?

Let’s also look at the exception handler for IOError, which shows the same issue as the previous one. The error message talks about failures to open the input file, but not about reading from it. Maybe that is the right thing to do in some piece of code, but most likely it isn’t.

My weak guideline — weak because I only apply it counted times— to clarify such a piece of code and make it more robust is to make try/catch blocks as narrow as possible. This indicates to the reader that you are aware that the contained piece of code raises the mentioned exceptions and no more, and that the code outside of the block does not raise such exceptions.

Following this rule, the above code snippet would look like this:

input_data = None
try:
    with open("list.txt", "r") as input_file:
        input_data = load_data_from_file(input_file)
except IOError, e:
    logging.error("Failed to load the input file")
    raise e
assert input_data is not None

connection = None
try:
    connection = Database.connect()
except DatabaseError, e:
    logging.error("Failed to connect to the database")
    raise e
assert connection is not None

data_for_import = prepare_data_for_import(input_data)

for entry in data_for_input:
    connection.add(entry)

Now it is obvious that connection.add most likely needs some kind of exception handling, as a write to an external system can always fail in some way. At this point, we are forced to decide whether we want to surround the single statement in a try/catch block, or instead we want to do so for the loop as a whole. This will depend on whether we want a fail-fast approach or a more greedy algorithm.

You may have noticed by now that the above looks pretty much like a poor man’s replacement for explicit error handling, and you may be right. Exceptions may sounds like a nice idea and, depending on the language, cannot be avoided… but getting them right and implementing accurate error handling with them is just really hard. Keep your code simple.

Comments from the original Blogger-hosted post: