A Good Way to Handle Errors Is To Prevent Them from Happening in the First Place
Error handling is a tricky part of software programming.
It’s tricky in several aspects: it’s difficult to get right, and it can make code less expressive.
But it doesn’t always have to be that way. Sometimes asking the question “how can we prevent the error from happening in the first place?” can avoid the need for error handling altogether.
Why handling errors is difficult
Several things make error handling in code difficult.
We haven’t got the right tool yet
If you look back on the history of C++, the number of tools for errors handling has been growing at a steady pace.
C used error numbers returned from a function to indicate whether an operation succeeded, and give an error ID to identify the reason why it failed.
C also provided errno
, a global indicator for an error number qualifying the last error that happened.
C++ came with exceptions, which allow to exit a function early if it can’t react to an error.
C++11’s error_code
improved on the concept of error codes.
C++17 brought std::optional
into the standard, allowing a function to return nothing in the case it failed to return what it was supposed to. This makes function interfaces more expressive.
C++20 almost had contracts (but they were taken out at the last moment). Contracts are a way to handle errors by checking the pre-conditions and post-conditions of an operation.
Looking back on this historical perspective, at any given point in time, better tools to handle errors were to be added in the future. So we never had optimal tools to handle errors.
It’s no different now. If we take a peek at what the future might hold for us in terms of error handling tools, there are at least two things.
First, Boost Outcome could well make it into a future version of the standard. Boost Outcome is a bit like std::optional
in that it allows a function not to return its result if it’s not able to. But contrary to std::optional
, Outcome embeds some information about what went wrong during the operation.
expected
, which is in the same spirit as Boost Outcome, has been proposed to the C++ standard.
Another component that could make it in the standard are static exceptions, which have been pushed forward by Herb Sutter. Herb talked about error handling and static exceptions in his ACCU 2019 keynote.
If you’d like to see a more detailed overview of C++ errors handling tools than this quick overview, Phil Nash made a good job of listing and comparing error handling strategies in his C++Now 2018 talk.
The point is, there are many tools to handle errors in C++, and more to come, and until we’re at the final version of C++, we will probably have to wait more to get yet a better tool to handle errors.
The current tools are difficult to use correctly
If we look back on the first error handling tools, such as error codes, they were quite difficult to use correctly. We couldn’t make sure the caller checked them at all, and if they checked them, they needed to be very careful not to mix up the error codes.
Exceptions made it impossible for their caller to ignore them, and made the qualification of the error more explicit. But they came with their constraints, in particular having exception safe code.
Making code exception safe is a good thing in itself, as exception safe code tends to have a better design. But there are lots of not-exception-safe code out there, which makes it difficult to use exceptions.
The more recent tools don’t have those issues, but whatever the tool we use to handle errors, we still need to write code to deal with them. This is code that is not in the “happy path”, which is the code that gets executed when no error is happening.
When we think about coding up a feature, we (or at least, myself) tend to think about the happy path. Whatever the tool we use, handling errors adds more complexity to the code, and makes it by itself more difficult to understand and creates potential sources for incorrect behaviour.
Preventing errors from happening in the first place
Another approach when designing a software solution for a problem is to prevent errors from happening in the first place.
When we struggle with handling errors in code, we can simply ask ourselves: “can we prevent this error from happening in the first place?”
The very fact of thinking about this possibility opens a range of solutions that we don’t explore when we focus on choosing between using exceptions or std::optional
.
One way to prevent errors from happening is by working on the user interface. In an application with a GUI, we can make small fixes that make certain combinations of configuration impossible to set up.
When the user chooses a certain option in the GUI, we can make the components react to that action. For example, we can open a modal window to force the user to go all the way through a series of choices or cancel that first option altogether. Or we can hide or disable some components to prevent the user from selecting inconsistent options.
I’m no UX expert, but several times a simple fix in the UI avoided us to put cumbersome error handling in our code. And it’s better for the user too: if no error happens, the user doesn’t get to see annoying error messages. The user is guided by the UI to make sure they stay in the “happy path”.
This has the advantage of making the user aware of what the happy path is. I’ve seen once an application was silently falling back on the happy path when the user selected a configuration the application wasn’t supporting. Even if this led to the application producing a “happy path” result, it silently fell back on a behaviour that the user didn’t ask for. This doesn’t look like a good solution overall.
I’m not saying that all errors can be handled by constraining the user interface. But some can, and every time it starts by asking the question “can we prevent this error from happening in the first place?”
Does your team usually think about this question? How do you prevent errors from happening in the first place?
You will also like
- A Concise Implementation of Fizzbuzz with std::optional
- Clearer interfaces with optional<T>
- Extract Function: Should I Extract the Condition Too?
- How to Make If Statements More Understandable
Share this post!