Functions: Functional Error Handling

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

  1. In the text that follows I'll refer to “functional error handling” as the acronym FEH.
  2. 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, and Some and None are subtypes of it
  • Your function returns a Some for the success case, and a None 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 or Either, 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")

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 or Either are when you want to return information about the error
    • Try returns the Exception
    • 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!