Because functional programming is like algebra, there are no null values or exceptions in FP code. But of course you can still have exceptions when you try to access servers that are down or files that are missing, so what can you do? This lesson demonstrates the techniques of functional error handling in Scala.
Scala’s Option/Some/None types
I already demonstrated one of the techniques to handle errors in Scala: The trio of classes named Option
, Some
, and None
. Instead of writing a method like toInt
to throw an exception or return a null value, you declare that the method returns an Option
, in this case an Option[Int]
:
def toInt(s: String): Option[Int] =
try {
Some(Integer.parseInt(s.trim))
} catch {
case e: Exception => None
}
Later in your code you handle the result from toInt
using match
and for
expressions:
toInt(x) match {
case Some(i) => println(i)
case None => println("That didn't work.")
}
val y = for {
a <- toInt(stringA)
b <- toInt(stringB)
c <- toInt(stringC)
} yield a + b + c
I discussed these approaches in the “No Null Values” lesson, so I won’t repeat that discussion here.
This post is sponsored by my new book,
Learn Functional Programming Without Fear.
Scala’s Try/Success/Failure types
Another trio of classes named Try
, Success
, and Failure
work just like Option
, Some
, and None
, but with two nice features:
Try
makes it very simple to catch exceptionsFailure
contains the exception message
Here’s the toInt
method re-written to use these classes. First, you have to import the classes into the current scope:
import scala.util.{Try,Success,Failure}
After that, this is what toInt
looks like:
def toInt(s: String): Try[Int] = Try {
Integer.parseInt(s.trim)
}
As you can see, that’s quite a bit shorter than the Option/Some/None approach. The REPL demonstrates how this works. First, the success case:
scala> val a = toInt("1")
a: scala.util.Try[Int] = Success(1)
Second, this is what it looks like when Integer.parseInt
throws an exception:
scala> val b = toInt("boo")
b: scala.util.Try[Int] = Failure(java.lang.NumberFormatException: For input string: "boo")
As that output shows, the Failure
that’s returned by toInt
contains the reason for the failure, i.e., the exception message.
There are quite a few ways to work with the results of a Try
— including the ability to “recover” from the failure — but common approaches still involve using match
and for
expressions:
toInt(x) match {
case Success(i) => println(i)
case Failure(s) => println(s"Failed. Reason: $s")
}
val y = for {
a <- toInt(stringA)
b <- toInt(stringB)
c <- toInt(stringC)
} yield a + b + c
Note that when using a for-expression and everything works, it returns the value wrapped in a Success
:
scala.util.Try[Int] = Success(6)
Conversely, if it fails, it returns a Failure
:
scala.util.Try[Int] = Failure(java.lang.NumberFormatException: For input string: "a")
Another way to write toInt
While I’m in the neighborhood, note that you can write toInt
much more concisely like this:
import scala.util.control.Exception._
def toInt(s: String): Option[Int] = allCatch.opt(s.toInt)
this post is sponsored by my books: | |||
#1 New Release |
FP Best Seller |
Learn Scala 3 |
Learn FP Fast |
Even more ...
There are other combinations of classes from third party libraries that you can also use to catch and handle exceptions, but Option/Some/None and Try/Success/Failure are my two favorites. You can use whatever you like, but I generally use Try/Success/Failure when dealing with code that can throw exceptions — because I almost always want the exception message — and I use Option/Some/None in other places, such as to avoid using null values.
For more information on functional error handling, see:
- My free Scala 3 “functional error handling” training video
- A cheatsheet on how to use higher-order functions (HOFs) with Option
- My “Big FP book,” Functional Programming, Simplified
- My “Little FP Book,” Learn Functional Programming Without Fear