Functions: Functional Error Handling (Scala 3 Video)
In Scala, we don’t throw exceptions. Instead, we handle errors with data types, what we call “error-handling data types”:
Option
Try
Either
We’ll look at Option
first, then Try
and Either
.
Notes
- In the text that follows I'll refer to “functional error handling” as the acronym FEH.
- Most of the following text comes from my book, Learn Functional Programming The Fast Way!
A (bad) “String to Int” method
The way I usually introduce FEH is to start with a function where things can go wrong, and the solution isn’t quite right:
// [1, wrong]
def makeInt(s: String): Int =
try
s.toInt
catch
case _: NumberFormatException => 0
Try using it:
makeInt("1") // looks good
makeInt("0") // looks good
makeInt("") // oops
makeInt("eleven") // wrong
Summary:
- The problem is that
0
isn’t always correct
A (correct) “String to Int” method (using Option
)
The correct approach is to use the Scala Option
type for the function result:
- Return
Option[Int]
Option
is the parent type, andSome
andNone
are subtypes of it- Your function returns a
Some
for the success case, and aNone
for the failure case
// [1, correct]
def makeInt(s: String): Option[Int] =
try
Some(s.toInt)
catch
case _: NumberFormatException => None
Once you have a result, use it with match
, for
, or getOrElse
, and other approaches:
val aString = "1" // test with: "1", "0", "", "hi"
makeInt(aString) match
case Some(i) => println(i)
case None => println("No match")
Using the Option
result in a for
expression:
// note that a/b/c are Int types in each block, and `for`
// returns an Option (`rez` is `Option[Int]`)
val rez: Option[Int] = for
a <- makeInt("1")
b <- makeInt("2")
c <- makeInt("3")
yield
a + b + c
When you want an “or else (otherwise)” solution for the failure case:
makeInt("1").getOrElse(0)
ADVANCED STUFF
In the following text, I share notes related to Try
and Either
that I did not cover in this introductory video.
I will cover this material later in the Advanced Scala video series, but I thought I’d share it here for anyone that is interested.
Other error-handling types (Try and Either)
The makeInt
type signature with the different error-handling types (Option
, Try
, Either
):
def makeInt(s: String): Option[Int]
def makeInt(s: String): Try[Int]
def makeInt(s: String): Either[Throwable, Int]
Note that you can write this particular function with
Try
orEither
, but because you don’t care about the actual exception, you just care about the value,Option
is generally used in this situation.
Try
example:
import scala.util.{Try, Success, Failure}
def makeInt(s: String): Try[Int] =
try
Success(s.toInt)
catch
case e: NumberFormatException => Failure(e)
// usage:
makeInt("1")
makeInt("hi")
- Note: The
Failure
constructor requires that it take aThrowable
parameter, see the Failure type scaladoc
Either
example:
def makeInt(s: String): Either[String, Int] =
try
Right(s.toInt)
catch
case e: NumberFormatException => Left("D’oh, got an NFE!")
makeInt("1")
makeInt("hi")
As shown, with Either
, the Left
value can contain any type: String
, exceptions/throwables, whatever makes sense for the problem at hand.
When to use Option, Try, or Either
- Use
Option
for “optional” results - Use
Try
orEither
are when you want to return information about the errorTry
returns theException
Either
is more flexible, can return anything
I usually use Try
or Either
when working with files, databases, and anything I access via a network (i.e., web services). This is because I usually want the detailed information about an error. For instance, when you attempt to access a resource across a network, you can get about ten different types of exceptions, including:
MalformedURLException
UnknownHostException
SocketTimeoutException
IOException
SSLHandshakeException
- (more)
Usually the code that calls your function will want to know what failed, and therefore we use Try
or Either
in these situations.
A more real-world Try example
import scala.io.Source
def readFile(filename: String): Try[String] = Try {
Source.fromFile(filename)
.getLines
.toList
.mkString("\n")
}
Advanced: allCatch versions
You can write shorter versions of those functions using a function called allCatch
:
import scala.util.{Try, Success, Failure}
import scala.util.control.Exception.*
def makeInt(s: String): Option[Int] =
allCatch.opt(s.toInt)
def makeInt(s: String): Either[Throwable, Int] =
allCatch.either(s.toInt)
def makeInt(s: String): Try[Int] =
allCatch.toTry(Try(s.toInt))
def makeInt(s: String): Try[Int] = Try(s.toInt)
def makeInt(s: String): Try[Int] = Try {
// comment
s.toInt
}
These functions work just like the previous examples.
As mentioned above, most of this text comes from my book, Learn Functional Programming The Fast Way!
Update: All of my new videos are now on
LearnScala.dev