Commit Logs

Scala Error Handling With Option, Try or Either

Error handling is one of those things that you probably don’t need to care too much when started with a programming language, but it will become super important once you want to do some serious stuff with it.

In a traditional imperative language, errors are mostly handled by a try and catch clause. For example, if we are reading something from DynamoDB, we can handle it using the following pattern

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
try {
readDDB()
} catch {
case e: Exception => throw new Exception(s"Exception caught: $e.")
}
```

Scala prefers more functional ways to handle errors and it provides a couple of options. Most commonly, `Option`, `Try` or `Either`.

## Option

`Option` is a powerful data type in Scala. It is mostly used to handle nullable values, but it can also be used to passing around exceptions when combined with `try` and `catch`. Admittedly, this is not a typical use case, but it often simplifies a great deal of downstream logic when there is only one possible type of exception.

Using `Option`, the DynamoDB example could be rewritten as

```scala
val ddbContentOption: Option[DdbContent] =
try {
Some(readDDB())
} catch {
case e: Exception =>
log.warn(s"Exception caught: $e.")
None
}

In this way, the response is of type Option and can be pass along to downstream logics and eventually be handled when it is used.

1
2
3
4
5
ddbContentOption.fold {
throw new exception("readDDB throws exception")
} { case c =>
useDdbContent(c)
}

Try

Use Option to pass along exceptions is easy, but also is very limited in terms of the types of exceptions being handled. A more powerful mechanism was introduced in Scala 2.10, i.e., the Try keyword.

Try can be used to wrap around methods, which results in an instance Try[A] that 1) if the computation is successful, it’s an instance of Success[A], simply wrapping the value of type A; and 2) if the computation errors out, it’s an instance of Failure[A], wrapping a Throwable.

Going back to our toy example, we can rewrite it as

1
2
3
import scala.util.Try

val ddbContent: Try[DdbContent] = Try(readDDB())

Working with Try values is very similar to Option - you can use all the typical functional sugar with it, such as getOrElse, map/flatMap and for comprehensions.

Specifically to Try, you can use isSuccess to check if the computation is successful; or use pattern matching to handle success and failure accordingly.

1
2
3
4
5
6
import scala.util.{Success, Failure}

ddbContent match {
case Success(lines) => lines.forEach(println)
case Failure(e) => log.warn(s"Exception caught: $e.")
}

Moreover, you don’t have to use getOrElse to set default values for Try. Instead, you could take advantage of the recover or recoverWith methods, which returns a Success by applying a partial function on the given Failure instance.

Either

Alternatively, people are also using Either for this purpose. But, similar to Option, Either has its usage outside of error handling.

Either takes two type parameters A and B. An instance of Either[A, B] is either an instance of A or an instance of B, which is defined by two sub types Left and Right. For example, an Either is a Left if it is an instance of A.

In error handling, the convention is to use the Left to represent the error case and Right for the success value. Therefore, our DynamoDB example can be wrapped using Either in this way

1
2
3
4
5
6
val ddbContent: Either[String, DdbContent] =
try {
Right(readDDB())
} catch {
case e: Exception => Left(e.getMessage)
}

In downstream, we can use pattern matching to handle success or failure.

1
2
3
4
ddbContent match {
case Left(msg) => log.warn(s"Exception caught: $msg.")
case Right(lines) => lines.forEach(println)
}

Unlike Option or Try, Either is unbiased, which means you need to choose the assumption that it is a Left or Right by calling .left or .right. Then you will get a LeftProjection or RightProjection as a left or right biased wrapper for the Either.

Summary

Scala provides a couple of nice APIs to work with error handling, such as Option, Try and Either. It prefers these functional style handling as opposed to the more traditional try and catch with side effects. With a few caveats, you can work with these APIs using standard funcitonal sugars.

  1. As always, I would really appreciate your thoughts/comments. Feel free to leave them following this post or tweet me @_LeiG.