Error handling is an issue that often comes up in our reviews. Different programs have different goals with respect to error handling. In a simple script it might be acceptable to just crash if an error occurs. The techniques we are showing here are for high reliability programs, where we want to ensure we handle a selected set of errors.
We have some example code below using an idiomatic Java way of handling invalid input
— throwing exceptions.
The issue with treating invalid input in this manner is it breaks type-safety.
Given the type signature
List[Int] => FavouriteNumbers
there is no way of telling that it may throw an exception.
Another way of saying this is the methods are partially defined on their inputs.
That is, there isn’t a valid return value for all input values.1
This issue mean we can not reason about the methods, which increases our cognitive load.
The solution is to encode the invariants into the type system. This means we move the validation of input into the types themselves, meaning we can only create valid instances. As a result, the compiler, rather than the runtime, will inform us if we attempt to instantiate an object with bad data.
How can we achieve this?
The requirement for
FavouriteNumbers is the input is a list that must contain at least one element.
Scalaz has just the thing we need —
As its name suggests it’s a list that is guaranteed to be non-empty.
We can rewrite
Angle can either succeed (with an
Angle) or fail (with an error message).
Scala provides what we need in the type
The value of
Either must be an instance of
Left is used for failure and
Right for success.
In our case a we fail with a
String or succeed with an
Rather than attempting to encode this for each of the classes implementing the trait,
we can make their constructors private and use a method on the companion object to enforce the requirements at instantiation.
Finally, there only ever needs to be a single instance of both
so let’s make them case objects.
We could use this same technique to improve our
FavouriteNumbers example, instead of using the
NonEmptyList type for the input.
This time using Scalaz’s implementation of
Either, called disjunction.
We can read the type of the disjunction just as we read
String \/ Angle is the same as
Scalaz also offers the sugar of
.left, which is nice.
In the examples above we are using a
String as the error type; normally we would use a richer type.
We now need to explicitly tell the compiler how we want to handle failure.
There are two typical ways to do this.
First, we can transform a result to a common type using
Second, we can fail fast.
map ignores the failure case and applies the function only to the success case:
We are now able to reason about our methods based on the type signatures. They are no longer partially defined functions — we now have a valid return value for all input values. We are encoding the error into the type signature, which forces the caller to think about and handle the failure case. This allows the compiler to help us.