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 — NonEmptyList[T]
.
As its name suggests it’s a list that is guaranteed to be non-empty.
We can rewrite FavouriteNumbers
as:
Creating an Angle
can either succeed (with an Angle
) or fail (with an error message).
Scala provides what we need in the type Either
.
The value of Either
must be an instance of Left
or Right
.
By convention Left
is used for failure and Right
for success.
In our case a we fail with a String
or succeed with an Angle
, giving: Either[String,Angle]
.
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 Perpendicular
and Straight
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 Either
’s.
String \/ Angle
is the same as Either[String,Angle]
Scalaz also offers the sugar of .right
and .left
, which is nice.
In the examples above we are using a String
as the error type; normally we would use a richer type.
Handling failure
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 fold
:
Second, we can fail fast.
map
ignores the failure case and applies the function only to the success case:
Conclusions
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.
-
It should be noted a partially defined function and a partial applied function are two quite different things. There is an excellent explaination on Stack Overflow. ↩