Scala’s Try class is broken. Not “broken” in the sense that it doesn’t fulfill its purpose: its purpose is to sandbox Throwables in an algebraic sum construct and provide a monadic interface over the sandbox. It does that just fine. Try is broken in the sense that one of its methods, _.toOption can return malformed Option values, violating our trust in the semantics of the Option class and leading to potential instability down the call stack.

Purpose of Try

If you are already familiar with Scala’s Try class, you might skip ahead.

The scala.util.Try class exists as a way to get a type-level representation for the result of operations that might throw. Take, for example, integer division, which throws when attempting to divide by zero.

def safeDivide: (Int     , Int    ) => Try[Int]
              = (dividend, divisor) => Try(dividend / divisor)

Here, since / might throw, we have wrapped the division inside the constructor for Try. More subtly, notice the type signature of safeDivide. We don’t return an Int, we return a Try[Int].

Try itself is a sealed abstract class, with exactly two subclasses, Success and Failure. A value of type Try[T] can either be a Success[T], which is simply a case class wrapping a T, or it can be a Failure[T], which doesn’t actually contain a T, but instead contains a Throwable. In code:

sealed abstract class Try[T] { ... }

final case class Success[T](value: T)
  extends Try[T] { ... }

final case class Failure[T](exception: Throwable)
  extends Try[T] { ... }

In general, we, as the caller of a procedure that might throw an exception, have a number of ways we can deal with the possibility of a thrown exception:

  • Let the program halt.
  • Handle the exception.
  • Pass the exception up the call stack.

Letting the program halt is okay if we’re at the top level, but not if we’re writing a library or utility functions that will be used frequently. Handling the error requires we provide a fallback value or return a null, which means we need to know something about what’s going to consume that value. Again, if you’re at the top level of your program you probably know what’s going to consume the value, but not if you’re writing a function that’s going to be used in all kinds of places.

So, for maximal code reuse, we simply want to pass the exception up the call stack and let the caller decide what to do with it (this is exactly the convention in, for example, node.js, where the first argument of every function is a placeholder for a possible exception).

Once we have one, how do we consume a value of Try? In node.js, every function begins with an explicit null check on the first argument. The analog of that in Scala would be case matching on the Try value.

val x: Int = ...
val y: Int = ...

val result: Try[Int] = safeDivide(x, y)

result match {
  case Success(v) => ...
  case Failure(e) => ...
}

In addition to explicit pattern matching, Try provides a monadic interface, with methods such as map and flatMap and with for notation. For example:

def divider: Try[Int] = for {
  x <- Try(StdIn.readLine("Enter the dividend:\n").toInt)
  y <- Try(StdIn.readLine("Enter the divisor:\n").toInt)
  result <- Try(x / y)
} yield result

StdIn.readLine(...).toInt can fail and throw, so we wrap it in a Try and if successful bind the successful values to x and y. Then we consume the x and y in x / y, which can also fail and throw, so we wrap that as well and yield it to the caller, wrapped in Success.

Here’s the important part: If anything inside a Try constructor throws, then instead of the program halting, the Try will resolve to Failure(e) where e is the thrown exception as a value of the class Throwable. Thus, any thrown exceptions get passed up to the caller, as was desired.

How Try is Broken and How To Use It Safely

Where Try exists to create a type-level abstraction for the results of operations that might throw, Option exists to create a type-level abstraction for values that might be null.

Much like Try, Option is an abstract class with two implementors, Some and None. A value of type Option[T] is either a Some[T] which wraps a T or the singleton None.

sealed abstract class Option[+T] { ... }

final case class Some[+T](value: T)
  extends Option[T] { ... }

object None extends Option[Nothing] { ... }

Option provides a monadic interface that I’m sure everyone in the world is familiar with by now, and it’s not all all much different from Try in that respect. What we’re interested in at the moment is the constructor for Option, found in its companion object.

object Option {
  ...
  def apply[T](x: T): Option[T] =
    if (x == null) None else Some(x)
  ...
}

The constructor codifies the semantics we expect of Option values, namely that the x in Some(x) cannot be null. If we have an Option[T] in our hands, and if it is not None, then we’re safe to extract the T and use it without the possibility of having a null T.

Try and Option both deal with failure scenarios, but a value of Try contains strictly more information than a value of Option, since Failure wraps a value whereas None doesn’t wrap anything. Because of this, there should be an obvious conversion from Try[T] to Option[T], and this is exactly what Try gives us in its _.toOption method.

sealed abstract class Try[T] {
  ...
  def toOption: Option[T] = this match {
    case Success(v) => Some(v)
    case Failure(e) => None
  }
  ...
}

Here’s the problem: _.toOption returns Some(v), but Try makes no guarantee that the v in Success(v) is not null. And it shouldn’t make such a guarantee. Try has one job, to pass thrown exceptions up the call stack, not to guard against null pointers, and Try does its one job well.

The problem is that Try’s _.toOption method calls Option’s implementor Some directly instead of calling Options’s default constructor. This can result in the creation of Some(v) values where the v is null, violating our expectations of Option values. The preferred implementation of _.toOption should replace the call to Some with a call to Option.

I can understand why the Scala developers would pick the implementation they did. The implementation of _.toOption they chose will only convert Failure values to None, giving a completely lossless translation of Success values to Some values. But information fidelity should not be the goal of such a method, especially when it violates our expectations of the Option class. The goal of the _.toOption method should be to return a value that behaves the way the user expects Option values to behave, and that means no null Some(v) values.

If we want to be able to reuse our code, we need to be able to count on the semantics of types like Try, for exception passing, and Option, for null guarding. The solution to the problem presented by Try’s _.toOption method is to either avoid its use or to flatMap over the unsafe Option value with the default constructor, both illustrated below:

val tryMe: Try[T] = ...

// Option 1: Avoid using `_.toOption`
val option1: Option[T] = tryMe match {
  case Success(v) => Option(v)
  case Failure(e) => None
}

// Option 2: Map the default constructor
val option2: Option[T] =
  tryMe.toOption.flatMap(Option.apply)

Either of those approaches will produce an Option value that conforms to our expectations.