Error Handling: Comparing Scala's Try with Scalaz's Either/Disjunction
Today we’re going to compare two ways of handling errors in Scala. First we will look at the type \/
from the Scalaz
library (yes, this is its “name”). This type is usually called Either
or Disjunction
. Next we will look at the Try
type from the Scala standard library. We go through the same example of implementing one function with both types. This will show their respective strengths and weaknesses.
A quick side note: You might ask yourself why i have not included the Either
type from the Scala standard library as well. It is a pity that both Scalaz and Scala have a type with this name as this causes a lot of confusion. Let me assure you that those should not be really compared as they have different use cases.
The example scenario
We are going to look at a small imaginary CMS application, that deals with users and their homepages. What we will try to do is authenticating a User and afterwards fetch his home page. Our domain model looks like this:
case class Homepage(title: String, content: String)
case class User(id: Long)
object DefaultHomePage extends Homepage("Welcome!", "This is your amazing Homepage!")
As you can see it is really simple, but it will be enough to explore different ways of error handling. Sometimes things go wrong in this application, because other required applications are not available. So we’ll need a DefaultHomePage
to display when we have trouble loading the page the user has requested.
The following are the functions/methods that are provided to us:
trait EitherService {
def authenticate(userId: String, secret: String): \/[MyError, User]
def fetchHomePage(user: User): \/[MyError, Homepage]
}
trait TryService {
def authenticate(userId: String, secret: String): Try[User]
def fetchHomePage(user: User): Try[Homepage]
}
As you can see someone was so kind to provide us with two different implementations. So we can explore both ways of handling errors. Our task is to write a function homePageForUser
for each version of the Service that accepts a userId and a secret and returns a Homepage
instance, which represents the home page of the user. The signatures of the new methods look like this:
// the function that uses the EitherService
def homePageForUser(userId: String, secret: String): \/[MyError, Homepage] = ???
// the function that uses the TryService
def homePageForUser(userId: String, secret: String): Try[Homepage] = ???
The existing functions are a really great foundation, we just need to build upon them.
The version based on Scalaz’s Either, Disjunction or \/
We are supposed to implement this function, while we have access to the variable service of type EitherService
.
val service: EitherService = ??? // let's not worry about its exact implementation
def homePageForUser(userId: String, secret: String): \/[MyError, Homepage] = ???
Let’s briefly talk about the return type of EitherService
. The return type indicates that we are getting either an error of type MyError
or the successful result in the form of a User
or a Homepage
. Accordingly our new function will also return either a MyError
instance or the Homepage
if it succeeded. The error side is a strong part of the Scalaz Either. It is clearly showing what kind of errors we should expect. We can simply lookup its declaration:
sealed trait MyError
case class UnknownUser(userId: Long) extends MyError {
override def toString = s"User with id [$userId] is unknown."
}
case class WrongSecret(userId: Long) extends MyError {
override def toString = s"User with id [$userId] provided the wrong secret."
}
case class ServiceUnavailable(service: String) extends MyError {
override def toString = s"The Service [$service] is currently unavailable"
}
It’s always a good idea to declare your errors as a sealed
type. This means you can extend this type only in the same source file. So at compile time all possible errors are known to you as a client. As we can see we might get back an error if a User
does not exist for a given id or the provided secret was wrong. And we might even get back a ServiceUnavailable
Error if EitherService
is not available at the moment (database or other required application is down). These are all the error cases we should think about and what to do when they occur.
But first let’s start with an implementation of the happy path ignoring the errors for now. This looks like an easy task with a for comprehension. We simply call authenticate
with the arguments we are given and afterwards go on to call fetchHomePage
with the user we received as a result in the first step.
def homePageForUser(userId: String, secret: String): \/[MyError, Page] = {
for {
user <- service.authenticate(userId, secret)
homePage <- service.fetchHomePage(user)
} yield homePage
}
Now let’s talk about error handling and the different errors that i have presented above. We can’t do much when a user is unknown or his secret is not valid. Assume that we currently have a hard time with our infrastructure. Therefore different services are unfortunately down very often. So we are getting very often a ServiceUnavailable
Error. Our method should handle this error case and just return the DefaultHomePage
object. This default page provides basic functionality so our users can do at least something.
If you have not dealt with Scalaz before the following code may look very strange to you. The \/
is the “name” of the type. This type has two possible subtypes: -\/
(called left) and \/-
(called right). Just look at the position of the -
and you will know whether it is right or left. By convention the left subtype is reserved for errors. With \/.left
and \/.right
we can create instances of this type.
We extend our implementation to analyze the result of the for comprehension before returning something. We pattern match the result to check wether it’s a left instance containing an ServiceUnavailable
error. If so we provide the DefaultHomePage
as a fallback. All other cases are returned unchanged (successful results and other errors).
def homePageForUser(userId: String, secret: String): \/[MyError, Page] = {
val homepage: \/[MyError, Page] = for {
user <- service.authenticate(userId, secret)
homePage <- service.fetchHomePage(user)
} yield homePage
homepage match {
case -\/(error: ServiceUnavailable) =>
\/.right(DefaultHomePage)
case _ =>
homepage
}
}
As you can see the type MyError
showed us clearly what could go wrong and we just had to go through those cases and decide what to do in each case.
The version based on Scala’s standard Try
Now we are going to implement the same thing again based on the TryService
. Let’s quickly recall its signature:
trait TryService {
def authenticate(userId: String, secret: String): Try[User]
def fetchHomePage(user: User): Try[Homepage]
}
We will start out with the happy path again:
def homePageForUser(userId: String, secret: String): Try[Homepage] = {
for {
user <- service.authenticate(userId, secret)
homePage <- service.fetchHomePage(user)
} yield homePage
}
This version looks very similar to the one based on Either
. Only the return types differ. This is because both types are monads and we use a for comprehension in both cases. Now let’s add the Error handling again. In this version we can’t tell from the type what errors we might encounter. With Try
we are relying on Exception
s for error handling. We dig through the source code and find the following exception hierarchy:
sealed class MyException(msg: String) extends Exception(msg, null)
case class UnknownUserException(userId: Long) extends MyException(s"User with id [$userId] is unknown.")
case class WrongSecretException(userId: Long) extends MyException(s"User with id [$userId] provided the wrong secret.")
case class ServiceUnavailableException(service: String) extends MyException(s"The Service [$service] is currently unavailable")
The good thing is the implementing developer was so kind to also use the sealed
keyword in this case. As we can see this hierarchy of exceptions is basically the same as the Either
based version. This time we handle the case of unavailable services by handling the ServiceUnavailableException
:
def homePageForUser(userId: String, secret: String): Try[Homepage] = {
val homepage: Try[Homepage] = for {
user <- service.authenticate(userId, secret)
homePage <- service.fetchHomePage(user)
} yield homePage
homepage match {
case Failure(e: ServiceUnavailableException) =>
DefaultHomePage
case _ =>
homepage
}
}
As with the previous version we pattern match on the result of the for comprehension. We go through the same cases. Just the name of the subtype for errors changes to Failure
. In the case of the ServiceUnavailableException
we return the fallback.
With Try
we could actually write it in a shorter way by replacing the pattern matching with the very convenient recover
method. We provide a PartialFunction
to this method, which matches on the Exception
and provides a fallback value. This fallback is only used if the Try
represents a Failure
and if the case matches.
homePageForUser.recover {
case e: ServiceUnavailableException => DefaultHomePage
}
So far this does look like as if the Try
is not as good as the Either
. It is not communicating the Errors we should think about. It justs offers the handy recover
method, which makes our code shorter. But we have to be more precise before coming to a conclusion. We need to differentiate between expected errors and unexpected errors. The Either
type is good at communicating expected errors. But we as programmers often deal with unexpected errors. Code is rarely perfect and therefore we should be prepared to deal with them. Let’s examine how those both types perform when dealing with unexpected errors. In this case the Try
will show its strengths.
Dealing with unexpected errors
The attentive reader might have spotted a small weird thing in the example code. The model User
does have an attribute id of type Long
. But our methods authenticate
and homePageForUser
actually declare the parameter userId as a String
. Does the method perform a conversion of the String
into a Long
? Let’s try what happens when we try both variants in a REPL. We start with the Either
version first:
// import our Either based example code
scala> homePageForUser("123", "my-secret!")
res0: scalaz.\/[try_vs_either.EitherExample.MyError,try_vs_either.Homepage] = \/-(Homepage(Your Homepage,Welcome to your Homepage!))
scala> homePageForUser("abc", "my-secret!")
java.lang.NumberFormatException: For input string: "abc"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:589)
As we can see the first one works, because everything is as expected. In the second case we suddenly run into an unexpected error and the call terminates with an exception. I know that this error may seem totally dumb and obvious to you, but it could be any other error you or someone else did not anticipate. Here we suddenly loose all goodness about error handling. Unexpected errors are suddenly not properly containerized anymore. This violates the holy grail of functional programming: referential transparency. The presumably more functional version based on Scalaz
is not so functional anymore in this case!
Not let’s try the same with the version based on Try
:
// import our Try based example code
scala> homePageForUser("123", "my-secret!")
res0: scala.util.Try[try_vs_either.Homepage] = Success(Homepage(Your Homepage,Welcome to your Homepage!))
scala> homePageForUser("abc", "my-secret!")
res1: scala.util.Try[try_vs_either.Homepage] = Failure(java.lang.NumberFormatException: For input string: "abc")
Here we see that this does not happen to Try
! The unexpected Error is still containerized. We get back a failure containing the unexpected exception. The value of this property cannot be underestimated on the JVM platform where exceptions are so widespread. Even if you are able to anticipate all possible errors and handle them in a proper way, you will still rely on other APIs that still rely on exceptions. In asynchronous applications this becomes even more important because unexpected errors can easily go undetected without proper containerization.
Conclusion
I often found myself in discussions where some FP advocate claimed that Scalaz’s Either
is better than Standard Scalas Either
(do not compare it to this one) and also better than the Try
type. Yes, the Scalaz Either
is better when you speak about expected errors. When you talk about unexpected errors the Try
is the winner. I hope this will be helpful to you the next time you think about error handling.
But the question is: Can we have a type that is both good at clearly communicating expected errors and also at dealing with unexpected ones? I think there actually is and in one of my upcoming posts i would like to show you how to implement your own enhanced Try
type, which combines the best of both worlds.
I would love to hear your feedback. If you like, follow me on Twitter.
PS: You can find the source code here.
blog comments powered by Disqus