Scala 3: Functions: Options and Functional Error Handling (Part 1)

NOTE: This is a chapter from my book, Learn Scala 3 The Fast Way. Due to a mistake, this lesson was not included in the book.

When you write functions, things can go wrong. In other languages you might throw exceptions or return null values, but in Scala you don’t do those things. (Technically you can, but other people won’t be happy with your code.)

Instead what we do is work with error-handling data types. To demonstrate this I’ll create a function to convert a String to an Int.

Background

Technically you can already convert a String to an Int already because the String class has a toInt method. The REPL shows how it succeeds and how it fails:

scala> "1".toInt
val res0: Int = 1

scala> "one".toInt
java.lang.NumberFormatException: For input string: "one"

In the first example the string properly represents an integer — "1" — so the conversion process works. But when the string can’t be converted to an integer in the second example, the process blows up with a NumberFormatException. This is bad.

A “String to Int” method

Knowing what we know so far, our first attempt to write a makeInt function might look like this:

def makeInt(s: String): Int = 
    try
        s.toInt
    catch
        case e: NumberFormatException => 0

That function will always return an Int, but the problem is that it isn’t accurate in all instances. For example, all of these method calls will return 0, which is incorrect:

makeInt("0")
makeInt("")
makeInt("zero")

In the first example the value returned is correct, but in the second and third examples, 0 is not the correct answer. So, what is the correct solution?

The correct solution

This is where Scala’s error-handling data types come in. Instead of the function returning an Int, the correct approach is for the function to return a data type that encapsulates the possibility of both success and failure. One such data type is the Option type, and its sub-types (children), Some and None.

The general solution is to (a) define your function to return an Option that wraps the desired return type — Int, in this case. And then in the body of the function, you return (b) a Some for the success case, and (c) a None for the failure case:

def makeInt(s: String): Option[Int] = 
    try
        Some(s.toInt)
    catch
        case e: NumberFormatException => None

As shown, the Some wraps the data type that you were able to calculate successfully, and in the error case you return a None. This is how the function works:

makeInt("1")      // Some(1)
makeInt("one")    // None

The key to this solution is that Option is a base type, and Some and None are its sub-types. So the function is defined to return the parent type: Option, or specifically an Option[Int] in this case. Then the body of the function returns either of those sub-types: Some for the success case, and None for the error/failure case.

Now that makeInt is written like this, it’s often used in a match expression:

makeInt(aString) match
    case Some(i) => println(s"Conversion worked. i = $i")
    case None => println("The conversion failed.")

The makeInt function can be written more concisely, but when you’re first starting out it helps to see this long form.

Other error-handling types

One problem with the Option/Some/None approach is that None doesn’t give the caller of your function any information on what went wrong. It just indicates “An error happened,” but doesn’t provide any details.

For situations where you want your function to give its users those details, Scala has these additional error-handling data types:

  • Try/Success/Failure
  • Either/Left/Right

These are demonstrated in the next lesson.

Exercises

The exercises for this lesson are available here.

More information

For a longer description, see my tutorial, Functional error handling in Scala.