Error Handling in Scala ======================= Scala does not have checked exceptions like Java, so you can't do soemthing like this to force a programmer to deal with an exception: ~~~ java public void stringToInt(String str) throws NumberFormatException { Integer.parseInt(str) } // This forces anyone calling myMethod() to handle NumberFormatException: try { int num = stringToInt(str); // do something with num } catch(NumberFormatException exn) { // fail gracefully } ~~~ In Scala we prefer to enforce error handling by encoding errors in the type system. How we encode the errors depends on what we want to achieve. Using `Option` -------------- In most cases we only need to know *if* something worked, not *why* it failed. In this case `Option` is an appropriate choice: ~~~ scala def stringToInt(str: String): Option[Int] = { try { Some(str.toInt) } catch { catch exn: NumberFormatException => None } } // Typical use case using `match`: stringToInt(str) match { case Some(num) => // do something with num case None => // fail gracefully } // We can also use `option.map()` and `option.getOrElse()`: stringToInt(str) map { num => // do something with num } getOrElse { // fail gracefully } ~~~ If we care about why an error happened, we have three options: 1. use `Either`; 2. use `Try`; 3. write our own class to encapsulate the result. Here's a breakdown of each approach: Using `Either` -------------- `Either[E, A]` has two subtypes, `Left[E]` and `Right[A]`. We typically use `Left` to encode errors and `Right` to encode success. Here's an example: ~~~ scala sealed trait StringAsIntFailure final case object ReadFailure extends StringAsIntFailure final case object ParseFailure extends StringAsIntFailure def readStringAsInt(): Either[StringAsIntFailure, Int] = { try { Right(readLine.toInt) } catch { catch exn: IOException => Left(ReadFailure) catch exn: NumberFormatException => Left(ParseFailure) } } // Typical use case using `match`: readStringAsInt() match { case Right(num) => // do something with num case Left(error) => // fail gracefully } // We can also use `either.right.map()`: readStringAsInt().right map { num => // do something with num } getOrElse { // fail gracefully } ~~~ Using `Try` =========== `Try[A]` is an odd one. It's like a special case of `Either[E, A]` where `E` is fixed to be `Throwable`. `Try` is useful when you're writing code that may fail, you want to force developers to handle the failure, and you *want to keep the exception around in the error handlers*. There are two possible reasons for this: 1. You need access to the stack trace in your error handlers (e.g. for logging purposes). 2. You want to print the exception error message out (e.g. to report and error in a batch job). Note that you only need `Try` if you need to *keep the exception around*. If you just need to catch an exception and recover, a `try/catch` expression is going to be just fine. Here's an example of using `Try`: ~~~ scala def stringToInt(str: String): Try[Int] = { Try(str.toInt) } // Use case: stringToInt(str) match { case Success(num) => // do something with num case Failure(exn) => log.error(exn) // fail gracefully } // Or equivalently: stringToInt(str) map { num => // do something with num } recover { case exn: NumberFormatException => log.error(exn) // fail gracefully } ~~~ Note that anything we write using `Try` can also be written using `Either`, although if using an instance of `Exception` to encapsulate our error, `Try` seems like a more natural fit. If we don't care about the error value, `Option` is always going to be simpler than `Either` or `Try`. Using custom error types ======================== Sometimes we need to return more information than simply *error-or-success*. In these cases we can write our own return types using sealed traits and generics. A good example is the `ParseResult` type used in Scala parser combinators ([API docs](http://www.scala-lang.org/api/2.10.4/index.html#scala.util.parsing.combinator.Parsers$ParseResult)). This encapsulates three possible results: - `Success[A]` -- The input was parsed successfully. The object contains a reference to the result of parsing. - `Failure` -- The input could not be parsed due to a syntax error. The object contains the line and column number of the failure, the next bit of text from the input stream and the expected token. - `Error` -- The parse code threw an exception. The object contains a reference to the exception. If we were implementing `ParseResult` ourselves, the code might look something like this: ~~~ scala sealed trait ParseResult final case class Success[A](result: A) extends ParseResult final case class Failure(line: Int, column: Int, /* ... */) extends ParseResult final case class Error(exception: Throwable) extends ParseResult // This is the signature of our parse() method: def parse[A](text: String): ParseResult[A] = // ... // And this a typical use case: parse[Int]("1 + 2 * 3") match { case Success(num) => // do something with num case Failure(/* ... */) => // fail gracefully case Error(exn) => // freak out } ~~~ Many different ways of skinning a cat ===================================== Here are a few different ways of writing a method that converts a `String` to an `Int` and forces developers to deal with parse errors. Read through these examples and consider the following questions: - Which approaches look best to you? Why? - Are there semantic differences to the code as well as stylistic ones? - What does your team think? Remember: - every codebase has a style; - the style can be different in different projects (that's ok); - the important thing is that the code is readable and maintainable *by the team writing/supporting it*; - in other words, *the team decides the style*! ~~~ scala def stringToInt: Option[Int] = { try { Some(readLine.toInt) } catch { catch exn: NumberFormatException => None } } def stringToInt: Option[Int] = { import scala.util.control.Exception._ catching(classOf[NumberFormatException]) opt { readLine.toInt } } def stringToInt(str: String): Option[Int] = { Try(str.toInt).toOption } def stringToInt(str: String): Try[Int] = { Try(str.toInt) } ~~~