Exception Handling in Scala

When I started working on scala I had a tough time understanding how exception handling works in scala. I had some background in java and javascript and I had exclusively worked with imperative codebases till that time.
When I look back I think my understanding about exception handling in scala came in stages: In the first stage I figured out what are the different ways of exception handling in scala. After this stage I knew what are different ways but I had a difficult time using those appropriately. During this time I started asking questions: Why do we need more than one way to handle exceptions? Where to use which one ? How to make the application behave properly in case of unexpected conditions and at the same time code should not become too messy for others to understand ? etc.

In this blog we will first briefly look into what are the different ways of exception handling in scala and when shall we use those. Next we will see some more points which are like set of best practices regarding Exception handling in scala.

There are predominantly 3 ways to handle exceptions in scala: Try, Option and Either.

From scaladoc The Try type represents a computation that may either result in an exception, or return a successfully computed value .A successful completion of Try[T] will result in Success[T] while failure results in Failure[T], where T is the result type. Failure wraps non-fatal exceptions. An example will look like as below:

val res = Try { /*some computation which can fail*/}
res match{
case Success(value) => SuccessResponse(value)
case Failure(exception) => {
Logger.error(" Your Failure Message",exception)
FailedResponse(exception.getMessage)
}
}

SuccessResponse and FailedResponse are case classes.
Use Try when you care about success or failure and also you care about matching the type of Throwable, getting error messages or state.

Option[T] is a container for Zero (None) or One element (Some[T])of type T.
One of the examples is the get method of Map which returns Option[T].
Use Option when you only care about success or failure but do not care about any error message or state.

Either[A,B] allows you to return one of two different types. Conventionally, Right is success and Left is failure.
Try can be thought of as Either with Left as Throwable and Right as result. Or Similarly Option can be thought of as Either where Left represents failure (by convention) and Right is akin to Some.
An example will look like as below:

trait GenericUserError
case class UserNotFound(id: Long) extends GenericUserError
case class UserAccountBlocked(id: Long) extends GenericUserError
case class User(id: Long, name: String)
def getUser: Either[GenericUserError, User] = ???
getUser match{
case Left(UserNotFound(id)) => println(s"user $id not found")
case Right(user) => println(s"username is ${user.name}")
}

I have found some people have issues with using Either in codebase and I think they are right because of the following reasons:

  • If we use Either for error handling, there is a certain assumption that Left is for Failure and Right is for Success cases. It’s easy to go wrong with this assumption in a large codebase with many people working. Either can do much more than simply error handling. In fact there is already an Either whose one of its two types is fixed for failure and another is for success. We should be using that for Exception handling. That is nothing but Try.
  • Either is not easily composable (no flatMap). In scala 2.12, there is a flatMap on Either which is right biased. Try and Option have been monadic in nature since their inception.
  • In case we want to return two results (success result and failed result) for a computation, Either can be used to simulate the same effect as try-catch block of java. But I think it’s better to subclass (using trait, case class, case object) our result types and make consumers handle all result types in such scenarios (rather than using Either).

After we have seen the different ways of error handling, below is a non exhaustive set of best practices for error handling in scala:

  1. Throwing Exceptions should be avoided. Scala does not have checked exceptions. Scala won’t mandate you to catch the thrown exception.
  2. Future is another useful tool in exception handling. If computation is long running, you can wrap that in Future. Try is free in the future.
  3. If you tempt to throw some exception from a computation down the stack , return Success(result) or Failure(YourCustomException). This will return Try and will give hint to the caller that Something can go wrong with this method.
    Alternatively, you can return Future.successful(result)/Future.failed(YourCustomException) which has the same effect with return type as Future.
  4. If a computation can fail, wrap that in Try.
  5. Fail as early as possible.

Software Engineer.