Scala 3 — Book

Functional Error Handling

Language

Functional programming is like writing a series of algebraic equations, and because algebra doesn’t have null values or throw exceptions, you don’t use these features in FP. This brings up an interesting question: In the situations where you might normally use a null value or exception in OOP code, what do you do?

Scala’s solution is to use constructs like the Option/Some/None classes. This lesson provides an introduction to using these techniques.

Two notes before we jump in:

  • The Some and None classes are subclasses of Option.
  • Instead of repeatedly saying “Option/Some/None,” the following text generally just refers to “Option” or “the Option classes.”

A first example

While this first example doesn’t deal with null values, it’s a good way to introduce the Option classes, so we’ll start with it.

Imagine that you want to write a method that makes it easy to convert strings to integer values, and you want an elegant way to handle the exception that’s thrown when your method gets a string like "Hello" instead of "1". A first guess at such a method might look like this:

def makeInt(s: String): Int =
  try {
    Integer.parseInt(s.trim)
  } catch {
    case e: Exception => 0
  }
def makeInt(s: String): Int =
  try
    Integer.parseInt(s.trim)
  catch
    case e: Exception => 0

If the conversion works, this method returns the correct Int value, but if it fails, the method returns 0. This might be okay for some purposes, but it’s not really accurate. For instance, the method might have received "0", but it may have also received "foo", "bar", or an infinite number of other strings that will throw an exception. This is a real problem: How do you know when the method really received a "0", or when it received something else? The answer is that with this approach, there’s no way to know.

Using Option/Some/None

A common solution to this problem in Scala is to use a trio of classes known as Option, Some, and None. The Some and None classes are subclasses of Option, so the solution works like this:

  • You declare that makeInt returns an Option type
  • If makeInt receives a string it can convert to an Int, the answer is wrapped inside a Some
  • If makeInt receives a string it can’t convert, it returns a None

Here’s the revised version of makeInt:

def makeInt(s: String): Option[Int] =
  try {
    Some(Integer.parseInt(s.trim))
  } catch {
    case e: Exception => None
  }
def makeInt(s: String): Option[Int] =
  try
    Some(Integer.parseInt(s.trim))
  catch
    case e: Exception => None

This code can be read as, “When the given string converts to an integer, return the Int wrapped inside a Some, such as Some(1). When the string can’t be converted to an integer, an exception is thrown and caught, and the method returns a None value.”

These examples show how makeInt works:

val a = makeInt("1")     // Some(1)
val b = makeInt("one")   // None

As shown, the string "1" results in a Some(1), and the string "one" results in a None. This is the essence of the Option approach to error handling. As shown, this technique is used so methods can return values instead of exceptions. In other situations, Option values are also used to replace null values.

Two notes:

  • You’ll find this approach used throughout Scala library classes, and in third-party Scala libraries.
  • A key point of this example is that functional methods don’t throw exceptions; instead they return values like Option.

Being a consumer of makeInt

Now imagine that you’re a consumer of the makeInt method. You know that it returns a subclass of Option[Int], so the question becomes, how do you work with these return types?

There are two common answers, depending on your needs:

  • Use a match expression
  • Use a for expression

Using a match expression

One possible solution is to use a match expression:

makeInt(x) match {
  case Some(i) => println(i)
  case None => println("That didn’t work.")
}
makeInt(x) match
  case Some(i) => println(i)
  case None => println("That didn’t work.")

In this example, if x can be converted to an Int, the expression on the right-hand side of the first case clause is evaluated; if x can’t be converted to an Int, the expression on the right-hand side of the second case clause is evaluated.

Using a for expression

Another common solution is to use a for expression—i.e., the for/yield combination that was shown earlier in this book. For instance, imagine that you want to convert three strings to integer values, and then add them together. This is how you do that with a for expression and makeInt:

val y = for {
  a <- makeInt(stringA)
  b <- makeInt(stringB)
  c <- makeInt(stringC)
} yield {
  a + b + c
}
val y = for
  a <- makeInt(stringA)
  b <- makeInt(stringB)
  c <- makeInt(stringC)
yield
  a + b + c

After that expression runs, y will be one of two things:

  • If all three strings convert to Int values, y will be a Some[Int], i.e., an integer wrapped inside a Some
  • If any of the three strings can’t be converted to an Int, y will be a None

You can test this for yourself:

val stringA = "1"
val stringB = "2"
val stringC = "3"

val y = for {
  a <- makeInt(stringA)
  b <- makeInt(stringB)
  c <- makeInt(stringC)
} yield {
  a + b + c
}
val stringA = "1"
val stringB = "2"
val stringC = "3"

val y = for 
  a <- makeInt(stringA)
  b <- makeInt(stringB)
  c <- makeInt(stringC)
yield
  a + b + c

With that sample data, the variable y will have the value Some(6).

To see the failure case, change any of those strings to something that won’t convert to an integer. When you do that, you’ll see that y is a None:

y: Option[Int] = None

Thinking of Option as a container

Mental models can often help us understand new situations, so if you’re not familiar with the Option classes, one way to think about them is as a container:

  • Some is a container with one item in it
  • None is a container, but it has nothing in it

If you prefer to think of the Option classes as being like a box, None is like an empty box. It could have had something in it, but it doesn’t.

Using Option to replace null

Getting back to null values, a place where a null value can silently creep into your code is with a class like this:

class Address(
  var street1: String,
  var street2: String,
  var city: String,
  var state: String,
  var zip: String
)

While every address on Earth has a street1 value, the street2 value is optional. As a result, the street2 field can be assigned a null value:

val santa = new Address(
  "1 Main Street",
  null,               // <-- D’oh! A null value!
  "North Pole",
  "Alaska",
  "99705"
)
val santa = Address(
  "1 Main Street",
  null,               // <-- D’oh! A null value!
  "North Pole",
  "Alaska",
  "99705"
)

Historically, developers have used blank strings and null values in this situation, both of which are hacks to work around the root problem: street2 is an optional field. In Scala—and other modern languages—the correct solution is to declare up front that street2 is optional:

class Address(
  var street1: String,
  var street2: Option[String],   // an optional value
  var city: String, 
  var state: String, 
  var zip: String
)

Now developers can write more accurate code like this:

val santa = new Address(
  "1 Main Street",
  None,           // 'street2' has no value
  "North Pole",
  "Alaska",
  "99705"
)
val santa = Address(
  "1 Main Street",
  None,           // 'street2' has no value
  "North Pole",
  "Alaska",
  "99705"
)

or this:

val santa = new Address(
  "123 Main Street",
  Some("Apt. 2B"),
  "Talkeetna",
  "Alaska",
  "99676"
)
val santa = Address(
  "123 Main Street",
  Some("Apt. 2B"),
  "Talkeetna",
  "Alaska",
  "99676"
)

Option isn’t the only solution

While this section focuses on the Option classes, Scala has a few other alternatives.

For example, a trio of classes known as Try/Success/Failure work in the same manner, but (a) you primarily use these classes when your code can throw exceptions, and (b) you want to use the Failure class because it gives you access to the exception message. For example, these Try classes are commonly used when writing methods that interact with files, databases, and internet services, as those functions can easily throw exceptions.

A quick review

This section was long, so let’s give it a quick review:

  • Functional programmers don’t use null values
  • A main replacement for null values is to use the Option classes
  • Functional methods don’t throw exceptions; instead they return values like Option, Try, or Either
  • Common ways to work with Option values are match and for expressions
  • Options can be thought of as containers of one item (Some) and no items (None)
  • Options can also be used for optional constructor or method parameters

Contributors to this page: