Checked Exceptions for reactive flow control
Sat, Feb 20, 2016 ❝On how checked exceptions are there to reactively switch to an alternative control flow path.❞Contents
In Why I prefer error values over exceptions we looked at the unfortunate characteristics of exceptions, among other things. As we discussed there, unchecked exceptions will automatically traverse up the call stack until they are caught. This makes them extremely useful as a way to reveal programming errors. Therefore it is best not to catch the general unchecked exception (such as Java’s RuntimeException
).
Unchecked exceptions should only occur in case of a programming error and on top of that they are not required to be declared. Catching a very generic type such as RuntimeException will most likely result in other exceptions accidentally being silenced. This is bad because, as a result, you will be hiding errors instead of fixing them. Futhermore, the programming error is not obvious anymore, but its side-effects (e.g. corrupted state, writing to a file left unfinished, etc.) may still be there.
On the other hand, it is not recommended to use unchecked exceptions for common alternative paths through your code. Whenever an alternative case can be expected, then you should not use unchecked exceptions. Common examples are events such as: losing a network connection, errors that happen while performing any kind of I/O, upon establishing a network connection finding out that the host is unreachable, while verifying data finding out that data is bad. These alternative cases should be part of your code. They are clearly not programming errors. Java’s checked exceptions provide a mechanism to do that where Java’s syntax provides you with hints on which code paths must be handled according to the known (declared) exceptions that may occur.
Kind of exception determined by context
The actual type of the exception depends not only on the type of error but very much on the context and the semantics of the method. Java’s IllegalStateException
is an unchecked exception that is thrown when you call a method at the wrong time, i.e. the object is in the wrong state and the method is not applicable right now. This is a programming error, as you forgot to call some other method first which puts the object in the correct state.
However, depending on your context, illegal state might be a common event. Take, for example, an implementation of a protocol which must process requests from the local user as well as the other party on the other end of the network connection. Processing happens concurrently as these are both independent actors. Effectively, it is very reasonable that processing happens in parallel. As a consequence there is a known race condition: messages from local user compete with messages from the network inside the protocol that processes the messages. The message that is received first, is processed first. This is independent of whether it originates from the local user or the party on the other end of the network connection. Now we have a common event that may only be discoverable at the earliest when the message is processed. The checked exception is there to define the alternative path that is (reactively) being taken when such an event occurs.
A similar case can be made for data verification. A method might assume that it receives a positive integer value. When a negative value is provided, a calculation fails and as a result an unchecked exception is thrown. The programming error is that we let the negative value slip through into that method. However, we can create a verification method which sole purpose is to do verification of the value. Now we would expect a checked exception to be thrown inside the verification method in case the data is bad. For a method that verifies data, this is a reasonable alternative outcome - an expected outcome - that must be handled accordingly.
Reactive flow control
Checked exceptions, because they are supported by the language, are a nice way to do reactive flow control. That is, you divert onto an alternative path of execution when such an exception occurs. Unchecked exceptions are inconvenient as you can easily forget to handle a possible alternative. Not to mention the case where a method gets modified and new alternative cases arise.
Checked exceptions are one step closer to your every-day error handling of expected alternative cases. You are forced, by the language, to handle defined alternatives. Concurrent cases can be handled reasonably well, without having to litter the code with if-statements all-over. Some of the other disadvantages of exceptions still hold, but checked exceptions most definitely provide advantages over unchecked exceptions.
Usage guidelines
Some general guidelines on the use of checked exceptions.
- Think carefully about a methods purpose: the context and intention determine whether an exception should be checked or unchecked.
- Every use case, that we need to respond to, should be defined by a checked exception.
- Any case that is clearly a sign of incorrect usage should be unchecked: we should not bother a user with checked exceptions for cases that should never happen.
- Remember that we want the programming language to support us in our goal of writing good, reliable source code. With checked exceptions we express our intention and the programming language will subsequently support us with it.
- Define few (typically only one) general checked exception from which we derive all exception cases that cross an abstraction boundary. That way, your user can pick the level of granularity that is needed for his use case.
This is mostly applicable to library writers. - Do not let checked exceptions from underlying abstractions leak through. Even if the error is not caused by your library itself, it still originates from inside your library. You may want to wrap the exception thrown by the underlying abstraction. Let the user decide on whether he cares about the root cause or not.
- And, finally, do not throw checked exceptions for use cases that should not happen. If something can only happen if the code is used incorrectly, then you should throw an unchecked exception.
Furthermore, static programming languages have the same goal of preventing incorrect usage of code by verifying types. If you can prevent bad usage patterns by using the type system to express these constraints and requirements, then this is preferred simply because the type system is verified at compile-time. That means that mistakes/bad usage are detected as soon as the application is compiled.