Cloud Consultant and Software Developer.
by Alex Dawkins
After giving feedback to a colleague that included “avoid using a broad try
when its not needed”, I thought it was worth writing out the problems with using try / except
.
try / except
?In programming languages, developers often have access to a mechanism that will allow them to attempt a command, and then handle the outcome in the case of an exception.
In Python, it looks like this:
try:
x = 5 / 1
except Exception:
print("That didn't work!")
x = 10
print(x)
try:
x = 5 / 0
except Exception:
print("That didn't work!")
x = 10
print(x)
# Output:
# 5
# That didn't work!
# 10
We can see here that if the code within the try block doesn’t cause an error, it runs as expected, and if it does cause an error (in this case caused by attempting to divide by zero.)
try / except
be avoided where possible?While this is a powerful tool and definitely has a lot of valid uses, it also can lead to subtle bugs that can take a long time to diagnose.
If we take the example from before and change it slightly, we can start to see why try
can cause some subtle bugs if used incorrectly.
def might_divide(a, b):
"""
This function will divide the first argument by the second.
If the second is 0, it will instead return the first argument.
"""
try:
return a / b
except Exception:
return a
Calling this function will work as expected:
might_divide(10, 2) # 5
might_divide(10, 0) # 10
However, if later on in development we refactor this code to look like:
def might_divide(a, c):
"""
This function will divide the first argument by the second.
If the second is 0, it will instead return the first argument.
"""
try:
return a / b
except Exception:
return a
The function now doesn’t behave as expected:
might_divide(10, 0) # 10
might_divide(10, 2) # 10
What? This function now returns an incorrect value, and isn’t doing “what is says on the tin”.
The problem is that we forgot to update the body of the function:
def might_divide(a, c):
"""
This function will divide the first argument by the second.
If the second is 0, it will instead return the first argument.
"""
try:
return a / b # should be a / c
except Exception:
return a
and the try block which was intended to handle the divide by zero error, instead caught the undeclared variable error.
A key thing to keep in mind is that errors are very, very helpful. While they may be frustrating to deal with, they are a clear indication that something has gone wrong in the code, and that we need to fix it. When we don’t have functional error handling, we let subtle inaccuracies propagate through our code.
This is also a major issue with using Large Language Models: it is very difficult to tell if they have made a mistake, and is made even harder by their perceived confidence.
On the line that includes except
, we follow this with Exception
, which is a generic Exception that is used by most exceptions in Python.
This means that almost all errors will be caught by this block, when really we only want the error which occurs when we divide by zero: ZeroDivisionError
.
If we update the function, now the bug is much more obvious to the developer.
def might_divide(a, c):
"""
This function will divide the first argument by the second.
If the second is 0, it will instead return the first argument.
"""
try:
return a / b
except ZeroDivisionError:
return a
might_divide(10, 0) # 10
might_divide(10, 2) # Error!
This will now show us an error when the unhandled exception occurs, and will make the mistake obvious and thus easier to fix.
Although it’s generally agreed in Python that it’s “Easier to Ask for Forgiveness than Permission” (EAFP), we can also handle the divide by zero error by preventing it from happening in the first place. This is called “Look Before You Leap” (LBYL).
def might_divide(a, c):
"""
This function will divide the first argument by the second.
If the second is 0, it will instead return the first argument.
"""
if c == 0:
return a
# We now know that it is safe to do the division, as c != 0
return a / c
might_divide(10, 0) # 10
might_divide(10, 2) # 12
Personally, I think this reads a bit nicer than try / except
, but it would require an additional comment like “prevents DivByZero error” to be as explicit as the other way, which is another Python convention.
In Terraform, for example, we can use try
like this:
locals {
# my_variable is expected to be a map with the key "key".
my_val = try(var.my_variable["key"], "default value")
}
Here, the intended purpose of this try function is to handle when the key doesn’t exist on the object, and for this, it works. However, if we rename the variable and forget to update this line, we will be in the same situation as before: another exception is being silenced, and the code is no longer behaving as expected.
Instead it is better to use the lookup
function, as it is more specific to the case that we’re trying to handle:
locals {
my_val = lookup(var.my_variable, key, "default value")
}
Similary, if we wanted to check that my_variable isn’t null we can use a conditional statement to LBYL:
locals {
my_val = var.my_variable == null ? "default value" : var.my_variable["key"]
}
try / except
try/except
is still the correct approach for things that we cannot anticipate, such as:
We can also use a broad except block to catch all exceptions in the case that we’re running something like a webserver, and we don’t want errors to end the execution. In this case, it is our own responsibility to alert the correct people about errors in the code, and recover gracefully.
We won’t cover it here, but python also offers a finally
block, which is equally useful and open to abuse.
While try / except
can be a powerful tool for developers, its misuse can lead to subtle bugs that are much harder to debug. If try / except
needs to be used, it should be restricted to only the intended exceptions to handle.
If it’s a simple case, or you cannot control which exceptions to catch, you should probaby use “Look Before You Leap” (LBYL) to perform checks before the potential exception is thrown.
We’ve mostly covered Python, and touched on Terraform as an example which doesn’t offer as rich exception handling, but these concepts can be applied to most if not all languages.
tags: Python