Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save letnotimitateothers/f36d46c40c94a02b4d8fa046b8c7fed6 to your computer and use it in GitHub Desktop.
Save letnotimitateothers/f36d46c40c94a02b4d8fa046b8c7fed6 to your computer and use it in GitHub Desktop.
Error handling in Scala

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:

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:

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:

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:

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). 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
}

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!

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)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment