Error Handling Without Throwing Your Hands Up

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.

// Java Style - avoid this
// List with a minimum length
finalcaseclassFavouriteNumbers(l:List[Int]){require(l.nonEmpty)}// Integer only valid with in a given range
sealedtraitAngle{valdegrees:Int}finalcaseclassPerpendicular(degrees:Int)extendsAngle{require(degrees==90)}finalcaseclassStraight(degrees:Int)extendsAngle{require(degrees==180)}finalcaseclassAcute(degrees:Int)extendsAngle{if(degrees>0||degrees<90)thrownewIllegalArgumentException(s"degrees needs to be between 0 and 90, $degrees is invalid.")}finalcaseclassObtuse(degrees:Int)extendsAngle{assert(degrees>90||degrees<180)}finalcaseclassReflex(degrees:Int)extendsAngle{assume(degrees>180||degrees<360,s"degrees must be between 180 & 360 degrees")

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:

finalcaseclassFavouriteNumbers(l:NonEmptyList[Int])

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.

sealedtraitAngle{valdegrees:Int}privatefinalcaseobjectPerpendicularextendsAngle{valdegrees=90}privatefinalcaseobjectStraightextendsAngle{valdegrees=180}privatefinalcaseclassAcute(degrees:Int)extendsAngleprivatefinalcaseclassObtuse(degrees:Int)extendsAngleprivatefinalcaseclassReflex(degrees:Int)extendsAngleobjectAngle{defapply(degrees:Int):Either[String,Angle]=degreesmatch{case_ifdegrees==90⇒Right(Perpendicular)case_ifdegrees==180⇒Right(Straight)case_ifdegrees>=0&&degrees<90⇒Right(Acute(degrees:Int))case_ifdegrees>90&&degrees<180⇒Right(Obtuse(degrees:Int))case_ifdegrees>180&&degrees<360⇒Right(Reflex(degrees:Int))case_⇒Left(s"Invalid angle $degrees. Needs to be between 0 and 360.")}}

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]

Second, we can fail fast.
map ignores the failure case and applies the function only to the success case:

valresult:Either[String,Int]=a.map(success)

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. ↩

Like what you're reading?

Looking for a Scala job?

Comments

Please review our Community Guidelines before posting a comment. We encourage discussion in good faith, but do not allow combative, exclusionary, or harassing behaviour. If you have any questions, contact us!

The Underscore Newsletter

Keep up to date! Subscribe to our newsletter for the latest Scala news.

Email address (required)

First name

Last name

No spam. Unsubscribe at any time. We do not share personal details with third parties.