Functional error handling in Scala - part 1
Let’s talk about error handling. In functional programming we don’t like side effects. Errors are exactly that - things that can (and will!) go wrong. How to handle errors in Scala in a functional way?
I mentioned in the very first post that when I think of Scala / functional programming I think algebra. What would your maths teacher have to say about you solving your equation like this:
…or…
If functional ways are like algebrae, then we can’t allow null values or exceptions. Null values break referential transparency rule, or in other words, they break the purity of a function. Exception do that too (to some extent). Let’s clarify.
What’s referential transparency and a pure function?
I mentioned it before but pure function is a function which:
- its return value is not
Unit
- think about it:Unit
strongly suggest we’re on to something impure (likeprintln
perhaphs?) - has its return value depend ONLY on its arguments:
- return value of this function will at all times without exception be the same
- the return value of this function can be used interchangeably with this function call
Referential transparency refers to the last point - in simplification, it means that you can always replace a function call with its return value.
No nulls sound sketchy
If you ever debugged NullPointerException
you will appreciate the no null rule. If not, think that null is not a value - it represents an absence of a value. And such thing doesn’t exist in algebra. Since the beginning of my functional programming journey I only came across using null in a context of working with some Java libraries that could return a null. Still then, my Scala code would not use null.
Scala does not have a notion of checked exceptions like Java, where the compiler reminds you (to a degree) of your error handling - so in Scala whatever you tell that your function will return, compiler assumes you aren’t lying.
Why exceptions break referential transparency “to some extent”?
Exceptions should be used according to their name - exceptionally. When a situation is a true exception. Is division by zero a true exception? No. If your recursive function unexpectedly blows up the stack, is it a true exception? Probably yes. (although that also means shame on your tests!)
That leads to (perhaps a controversial) statement that exceptions break referential transparency only if they’re explicitly observed and acted upon. That means - if you write code that throws or catches exceptions explicitly, you break referential transparency. If the code does it “on its own” - I personally think it’s fine. The exception will be thrown in a truly exceptional situation then. That obviously doesn’t mean we ignore error handling altogether! Let’s see what Scala has to offer.
Option and its usage in a real world
Option
suggest an optional value - a value that might, or might not exist. If it exists it’s wrapped in Some
, if it doesn’t - it becomes None
. This goes very well with pattern matching.
Let’s have a look at basic usage below - imagine optional fields in registration, like “birthday”. calculateAge
function takes a date of birth and calculates the age.
If our customer added their birth date their age will be calculated and returned wrapped in Some
. If customer is missing their date of birth the calculateAge
won’t even be called and their age will be returned as None
. It’s up to the caller now to deal with it.
Option is used a lot. Some practical examples:
- optional arguments in functions:
- arguments that are not present on initialize
- when you don’t care about the error message
Summary
Option
== possible absence of data OR we don’t care about error message
Either and its usage in a real world
Either
is very similar to Option with a difference that it allows to retrieve an error message. It can be either Left
or Right
- Left
suggests a problem (similar to None
from Option
), and Right
suggest everything went ok (similar to Some
for Option
).
Let’s have a look at the birthday example from above implemented using Either
.
Either
is used a lot, I would say about the same as Option
. Of course there is a ton of much better ways to implement calculateAge
- there is no doubt about it, but let’s use it to grind out a few good ideas on how and when I use Either
in real life.
- when you want to get the error message and act on it you package it in
Left
.Right
will represent the happy path. - Use
Either
instead of throwing exceptions - you can create a case class (or a whole hierarchy even, kinda like with exceptions) to hold your error message as I did above (that’s a common practice too) - make sure your
Either
is comprehensible and easy to reason about in a context it appears in. That means there is times when it’s better to create types or aliases and haveEither[DatabaseConnectionProblem, WriteSuccess]
instead ofEither[Problem, Unit]
- think how easy or difficult it is to place firstEither
vs secondEither
in a context of what your code does.
Summary
Either
== possible failure of operation AND we care about error message
Try and its usage in a real world
Last but not least (although used the least from all three types I mentioned today) is Try
.
You can think of Try
as a specialized kind of Either
where Left
is not Left
but Throwable
. That means Try
is a great option for working with code that throws exceptions (think Java libraries). The constructor automatically “catches” the exceptions should they be thrown and places them in Try
s equivalent of Left
- Failure
. Happy path outcome goes into Success
.
Using Try
with our calculateAge
would be a bit of an overkill, so let’s have a look at the division again:
…think how easy it would be to pattern match to handle this and this way break out from bubbling the exception up:
There is no silver bullet but generally Try
is used a bit less than Either
and Option
for a simple reason that we generally don’t expect to see exceptions in Scala code. Try
is a great choice for:
- code that throws exceptions (Java libraries)
Summary
Try
== code wrapped in Try
could throw an exception
Should I ever throw exceptions then?
As mentioned above - only in truly exceptional situations. I’m a purist when it comes to defining an exception (in sense of a throwable) - if you know it can happen in your code, it’s not an exception, it’s just an unhappy path. And as throwing an exception in Scala code is a big big deal for me I remember very clear there was only one time in the duration of my career where an explicit exception was allowed in the Scala codebase I worked on.
Error handling summary
When you think about it, following referential transparency blindly would eventually lead us to think that we can’t have any IO operations / data stores, not even random numbers or time/date operations. What’s the use of programming then?
As I mentioned a number of times in other posts already, it is all down to how much of a purist you want to be. In my code I follow those rules:
- I don’t use nulls. Ever. I use
Option
if I don’t care why something might beNone
, andEither
if I want to be able to know the failure reason - I don’t throw exceptions. I use
Either
instead. - I don’t try/catch exceptions from code that could throw them (e.g. Java libraries) - instead I wrap the offending code in
Try
and pattern match onSuccess
orFailure
- that’s where the exception stops bubbling up - I think of exceptions and nulls as evil - I do whatever it takes not to use them, even if that means seeking help from my senior colleagues on alternatives I might not (yet!) be aware of.
(Not so) scary stuff
Remember how in the post on for comprehensions I said a monad is something that can be flatmapped over? I said:
For comprehensions are great for working with and composing monads.
Guess what - Try
, Option
and Either
(although Either from Scala 2.12 only!) are also monads. That means you can use them in for comprehensions.
References:
Scala Cookbook by A. Alexander
Programming in Scala: Simplified by A. Alexander
https://www.garysieling.com/scaladoc/