Error handling – categorization and best practices
While working with different teams in the Java world, I found that many people intuitively know how to deal with software errors. I found it fascinating that so many people came with the same solution, in so many different places. This must be some sort of a pattern that we are yet to name, since we have already named Pokemon exception handling.
Even though the idea for this article came from Pokemon exception handling, in order to convince people not to use it, I had to truly understand what errors in software development are. I came up with a list of three main categories of errors:
- Configuration errors
- Developer errors
- Domain boundaries
Prerequisites to run a program
When an app can’t run, in most cases, it needs to fail. These means that some prerequisite hasn’t been met and something has to be done in order to run the program. For example, make that MySQL database accessible, configuration file is missing, port is taken, etc. Usually, these are errors we fix once. In Java world, Errors are used to denote these abnormal conditions.
A category of errors created by the developers. We could argue that all software errors are developer induced errors, but these errors are somewhat specific. If one doesn’t check for a null (or use null altogether), one is going to be bitten hard. In production.
Not conforming to domain logic, or logical bugs, are also developer errors. They allow the software state to be in a forbidden state. Having bugs like this for too long in production can be really expensive to fix.
These errors are a result of lack of developer testing. They don’t exercise the code in all possible states, for different reasons. Not knowing the allowed state of the application is only one of them, but many times it is sloppiness. This practice is also known as pain driven development, since test driven development is <sarcasm>soooo expensive</sarcasm>.
In Java, we know these errors as Runtime exceptions.
<rant>While on the subject of nulls, I’d like to mention that IntellIJ and Eclipse have had enough of them and introduced nullity annotations, since JSR 305 has greatly been ignored.</rant>
Believe it or not, these are the errors developers make intentionally. We make these errors because software state is forbidden by the domain logic. For example, a certain role cannot perform a certain action, or we the system should not create a credit line for customers without a good credit history. These are intentional and we know them as business rules, even though they are errors in the program flow. Or usually they are.
Error handling strategies
As you noticed, I try to avoid the word Exception, since error handling can be done without them. Hopefully, returning integers to determine success is long behind us.
There are two error handling strategies that are alive today that make sense, at least to me.
One strategy is with exceptions and the other one is never to break the flow and return either a result or an error (or both).
Returning an error
I tried this once in Java and gave up quickly. The thing is: if it’s not built in the language, you will not accomplish what you want. Truth to be told, we tried to use these to force people to handle errors. I tried with generics and created something like Either. Next thing I know, the user of my class called toJson on it; and since these tools work so good with generics, not even a warning was generated and my point was lost. It’s said, but it’s no surprise since we programmers are the laziest species known to human-kind.
As said, it might make sense if it’s built-in the language, reflection does not work on it or it’s a functional language you are working with.
Most of us use Exceptions for error handling. This is the part we, intuitively, got correct. Well most of us for what it’s worth. Here, I’d like to introduce the rules for good exception handling that I have gathered over the years:
- Thy shall NEVER catch an exception if I can’t recover from it
- Thy shall NEVER throw generic exceptions
- Thy shall NEVER swallow all exceptions
- Thy shall HANDLE unhandled exceptions on the client
Catching an exception that we can’t recover from will lead to inconsistent state. Catch it if you have a backup mechanism, a retry mechanism, or you want to wrap it in another exception. Otherwise, don’t catch it at all. In Java, we have these checked exceptions which we must catch or propagate. It started as a good idea, but today hardly anyone use them, since the point was lost a long time ago.
Throwing generic exceptions makes it really hard to do proper exception handling. If, for every error, you throw a RuntimeException (for fun sake: with NO message), the client, where you must handle exceptions, will not know what to tell the user of the program. Create a simple wrapper for the exception will solve you a lot of time.
If you gotta catch them all and not doing anything with the exception (i.e. swallowing it) you are introducing a developer error. It is inevitable and only a dead, unmaintained program is not going to introduce it. So, never swallow exceptions.
OK, but where do we handle errors? Where it matters! It’s usually on the client, which can mean anything from a REST endpoint, REST client, web page, desktop or mobile UI. The client must know if something has gone wrong and should decide what to do with it. The client CAN swallow an exception, but use it sparingly.
That’s not all folks!
This article is supposed to start a discussion on the subject. Help improve!