Try, Option or Either?

18 Feb, 2015
Xebia Background Header Wave

Scala has a lot of different options for handling and reporting errors, which can make it hard to decide which one is best suited for your situation. In Scala and functional programming languages it is common to make the errors that can occur explicit in the functions signature (i.e. return type), in contrast with the common practice in other programming languages where either special values are used (-1 for a failed lookup anyone?) or an exception is thrown. Let's go through the main options you have as a Scala developer and see when to use what!

Option A special type of error that can occur is the absence of some value. For example when looking up a value in a database or a List you can use the find method. When implementing this in Java the common solution (at least until Java 7) would be to return null when a value cannot be found or to throw some version of the NotFound exception. In Scala you will typically use the Option[T] type, returning Some(value) when the value is found and None when the value is absent. So instead of having to look at the Javadoc or Scaladoc you only need to look at the type of the function to know how a missing value is represented. Moreover you don't need to litter your code with null checks or try/catch blocks. Another use case is in parsing input data: user input, JSON, XML etc.. Instead of throwing an exception for invalid input you simply return a None to indicate parsing failed. The disadvantage of using Option for this situation is that you hide the type of error from the user of your function which, depending on the use-case, may or may not be a problem. If that information is important keep on reading the next sections. An example that ensures that a name is non-empty: [scala] def validateName(name: String): Option[String] = { if (name.isEmpty) None else Some(name) } [/scala] You can use the validateName method in several ways in your code: [scala] // Use a default value validateName(inputName).getOrElse("Default name") // Apply some other function to the result validateName(inputName).map(.toUpperCase) // Combine with other validations, short-circuiting on the first error // returning a new Option[Person] for { name <- validateName(inputName) age <- validateAge(inputAge) } yield Person(name, age) [/scala] Either Option is nice to indicate failure, but if you need to provide some more information about the failure Option is not powerful enough. In that case Either[L,R] can be used. It has 2 implementations, Left and Right. Both can wrap a custom type, respectively type L and type R. By convention Right is right, so it contains the successful result and Left contains the error. Rewriting the validateName method to return an error message would give: [scala] def validateName(name: String): Either[String, String] = { if (name.isEmpty) Left("Name cannot be empty") else Right(name) } [/scala] Similar to Option Either can be used in several ways. It differs from option because you always have to specify the so-called projection you want to work with via the left or right method: [scala] // Apply some function to the successful result validateName(inputName) // Combine with other validations, short-circuiting on the first error // returning a new Either[Person] for { name <- validateName(inputName).right age <- validateAge(inputAge).right } yield Person(name, age) // Handle both the Left and Right case validateName(inputName).fold { error => s"Validation failed: $error", result => s"Validation succeeded: $result" } // And of course pattern matching also works validateName(inputName) match { case Left(error) => s"Validation failed: $error", case Right(result) => s"Validation succeeded: $result" } // Convert to an option: validateName(inputName).right.toOption [/scala] This projection is kind of clumsy and can lead to several convoluted compiler error messages in for expressions. See for example the excellent and in detail discussion of the Either type in the The Neophyte's Guide to Scala Part 7. Due to these issues several alternative implementations for a kind of Either have been created, most well known are the \/  type in Scalaz and the Or type in Scalactic. Both avoid the projection issues of the Scala Either and, at the same time, add additional functionality for aggregating multiple validation errors into a single result type. Try Try[T] is similar to Either. It also has 2 cases, Success[T] for the successful case and Failure[T] for the failure case. The main difference is that the failure can only be of type Throwable. You can use it instead of a try/catch block to postpone exception handling. Another way to look at it is to consider it as Scala's version of checked exceptions. Success[T] wraps the result value of type T, while the Failure case can only contain an exception. Compare these 2 methods that parse an integer: [scala] // Throws a NumberFormatException when the integer cannot be parsed def parseIntException(value: String): Int = value.toInt // Catches the NumberFormatException and returns a Failure containing that exception // OR returns a Success with the parsed integer value def parseInt(value: String): Try[Int] = Try(value.toInt) [/scala] The first function needs documentation describing that an exception can be thrown. The second function describes in its signature what can be expected and requires the user of the function to take the failure case into account. Try is typically used when exceptions need to be propagated, if the exception is not needed prefer any of the other options discussed. Try offers similar combinators as Option[T] and Either[L,R]: [scala] // Apply some function to the successful result parseInt(input).map( * 2) // Combine with other validations, short-circuiting on the first Failure // returning a new Try[Stats] for { age <- parseInt(inputAge) height <- parseDouble(inputHeight) } yield Stats(age, height) // Use a default value parseAge(inputAge).getOrElse(0) // Convert to an option parseAge(inputAge).toOption // And of course pattern matching also works parseAge(inputAge) match { case Failure(exception) => s"Validation failed: ${exception.message}", case Success(result) => s"Validation succeeded: $result" } [/scala] Note that Try is not needed when working with Futures! Futures combine asynchronous processing with the Exception handling capabilities of Try! See also Try is free in the Future. Exceptions Since Scala runs on the JVM all low-level error handling is still based on exceptions. In Scala you rarely see usage of exceptions and they are typically only used as a last resort. More common is to convert them to any of the types mentioned above. Also note that, contrary to Java, all exceptions in Scala are unchecked. Throwing an exception will break your functional composition and probably result in unexpected behaviour for the caller of your function. So it should be reserved as a method of last resort, for when the other options don’t make sense. If you are on the receiving end of the exceptions you need to catch them. In Scala syntax: [scala] try { dangerousCode() } catch { case e: Exception => println("Oops") } finally { cleanup } [/scala] What is often done wrong in Scala is that all Throwables are caught, including the Java system errors. You should never catch Errors because they indicate a critical system error like the OutOfMemoryError. So never do this: [scala] try { dangerousCode() } catch { case => println("Oops. Also caught OutOfMemoryError here!") } [/scala] But instead do this: [scala] import scala.util.control.NonFatal try { dangerousCode() } catch { case NonFatal() => println("Ooops. Much better, only the non fatal exceptions end up here.") } [/scala] To convert exceptions to Option or Either types you can use the methods provided in scala.util.control.Exception (scaladoc): [scala] import scala.util.control.Exception. val i = 0 val result: Option[Int] = catching(classOf[ArithmeticException]) opt { 1 / i } val result: Either[Throwable, Int] = catching(classOf[ArithmeticException]) either { 1 / i } [/scala] Finally remember you can always convert an exception into a Try as discussed in the previous section. TDLR;

  • Option[T], use it when a value can be absent or some validation can fail and you don't care about the exact cause. Typically in data retrieval and validation logic.
  • Either[L,R], similar use case as Option but when you do need to provide some information about the error.
  • Try[T], use when something Exceptional can happen that you cannot handle in the function. This, in general, excludes validation logic and data retrieval failures but can be used to report unexpected failures.
  • Exceptions, use only as a last resort. When catching exceptions use the facility methods Scala provides and never catch { _ => }, instead use catch { NonFatal(_) => }

One final advice is to read through the Scaladoc for all the types discussed here. There are plenty of useful combinators available that are worth using.

Explore related posts