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:
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.
In most cases we only need to know if something worked, not why it failed. In this case Option
is an appropriate choice:
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:
- use
Either
; - use
Try
; - write our own class to encapsulate the result.
Here's a breakdown of each approach:
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:
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
}
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:
-
You need access to the stack trace in your error handlers (e.g. for logging purposes).
-
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
:
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
.
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). 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:
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
}
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!
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)
}
Nice. One thing that's missing is a solution for validating input and that can return multiple errors instead of just one (something that isn't addressed by exceptions either). A very common use-case is - form validations.
Maybe you could share your thoughts / experience on that as well.