Thinking With Types: Functional error handling in Scala

Once you start writing pure functions in Scala, it’s important to understand this rule:

  Pure function signatures don’t lie.

What you see is what you get. And when it comes to handling exceptions that can occur in functions, well, this means that pure functions don’t throw exceptions.

So what do they do? They return one of several different types. These are the error-handling types that are built into Scala:

  • Option
  • Try
  • Either

There are other types in third-party libraries, but these are the built-in error handling types in the Scala library.

What this looks like

For example, when you attempt to convert a string to an integer in Scala, you have to account for the possibility of getting a string like "foo", so a makeInt function will have signatures like these:

def makeInt(s: String): Option[Int]
def makeInt(s: String): Try[Int]
def makeInt(s: String): Either[Throwable, Int]

Whoa. Now we’re seeing more complex types:

Option[Int]
Try[Int]
Either[Throwable, Int]

I’ll come back to this in a few moments, but for the present moment, it’s important to note that these types are already as difficult to grok (comprehend) as anything I ever wrote in Java.

Getting back to the point at hand ... while these types require the brain to work a little harder, they’re also cool because the function signature doesn’t lie, and indeed, it tells you what you need to know:

  • The makeInt process can fail
  • If it fails, the error information will be inside those Option, Try, and Either types

So, once you know how Option, Try, and Either work, you’re in great shape! Pure function signatures don’t lie, and you can read them.

Furthermore, because Option, Try, and Either work well in Scala for-expressions, you don’t have to write a bunch of try/catch code to deal with these types.

It’s just algebra

One thing that’s important to mention is that if this code looks scary:

def makeInt(s: String): Option[Int]
def makeInt(s: String): Try[Int]
def makeInt(s: String): Either[Throwable, Int]

it may help to know that it’s just math, like algebra, high school algebra. Let’s say you use that last function, the one written with Either. When you call it, your code looks like this:

val i = makeInt(s)                           // don’t declare i’s type
val i: Either[Throwable, Int] = makeInt(s)   // explicitly declare i’s type

The first example shows how you call makeInt without declaring the type of i, and the second example shows how you declare i’s type explicitly. The two lines are equivalent.

If you ignore types and classes completely for a few moments, let’s just imagine that makeInt returns a tuple:

val (e, i) = makeInt(s)

Now, if you further write that code like this:

(e, i) = makeInt(s)

you can see what I’m talking about: this is just algebra. The equation states that we’re passing the parameter s into a pure function named makeInt, and makeInt returns two values, e and i, where e stands for the possible error that can occur, and i is the value you get when all goes as expected.

The cool thing here is that if you like algebra, you’re really going to like pure functions. All of your code becomes a series of equations.

In fact, what happens is:

  • You write all of your logic as a series of pure functions
  • Then you combine those pure functions together, just like combining a series of algebraic equations together

books by alvin