Don’t accidentally catch exceptions

It is commonly known that catching broad exceptions, such as Exception in a try...except block leads to issues around catching exceptions that should not have been caught and then are not handled correctly in the except block. A related issue is encapsulating too much code in a try...except block which can lead to catching exceptions on unexpected lines of code. This post provides an example where an except block unexpectedly catches an exception and gives an example for how to avoid the issue.

Take a look at the following example where the try...except block in the get_name_key function encapsulates a local operation and a function call:

def check_id_valid(source: dict):
    id_ = source["id"]
    if not isinstance(id_, str) and not id_.isnumeric():
        raise InvalidIdError()

def get_name_key(source: dict):
    try:
        check_id_valid(source)
        name = source["name"]
    except KeyError:
        raise NameNotFoundError()

Calling get_name_key({}) will result in the NameNotFoundError error being raised although not because the name key is not in source – it actually gets raised because id is not in the source. That is confusing! There are a few changes we should make to the code to clean it up.

Firstly, the try...except block raising NameNotFoundError should be scoped to just include where KeyError related to the name key can actually occur:

def check_id_valid(source: dict):
    id_ = source["id"]
    if not isinstance(id_, str) and not id_.isnumeric():
        raise InvalidIdError()

def get_name_key(source: dict):
    check_id_valid(source)
    try:
        name = source["name"]
    except KeyError:
        raise NameNotFoundError()

Now we get the expected behaviour which is that a KeyError is raised indicating that id is not a key in source. The second change we should make is to also handle KeyError in the check_id_valid to function to improve the developer experience:

def check_id_valid(source: dict):
    try:
        id_ = source["id"]
    except KeyError:
        raise IdNotFoundError()
    if not isinstance(id_, str) and not id_.isnumeric():
        raise InvalidIdError()


def get_name_key(source: dict):
    check_id_valid(source)
    try:
        name = source["name"]
    except KeyError:
        raise NameNotFoundError()

If KeyError had been handled in check_id_valid in the first place, we would not have encountered this issue. However, code isn’t always as simple as the above example and the try...except statement should both scope the exceptions and the code it contains to specifically what it can handle and not let other errors that are not properly handled slip into the except flow. This will avoid accidentally handling errors that the except block is not designed to handle.

Leave a comment