1

Let's say that I have a method addUser that adds a user to database. When called, the method might:

  1. succeed
  2. fail, because the input was invalid (i. e. the user name already exists)
  3. fail, because the database crashed or whatever

The method would probably consist of a single database API call that would in case of failure throw an exception. If it was in plain Java, I'd probably catch the exception inside the method and examine the reason. If it fell in the second category (invalid input), I would throw a custom checked exception explaining the reason (for example UserAlreadyExistsException). In case of the second category, I'd just re-throw the original exception.

I know that there are strong opinions in Java about error handling so there might be people disagreeing with this approach but I'd like to focus on Scala now.

The advantage of the described approach is that when I call addUser I can choose to catch UserAlreadyExistsException and deal with it (because it's appropriate for my current level of abstraction) but at the same time I can choose to completely ignore any other low-level database exception that might be thrown and let other layers deal with it.

Now, how do I achieve the same thing in Scala? Or what would be the right Scala approach? Obviously, exceptions would work in Scala exactly the same way but I came across opinions that there are better and more suitable ways.

As far as I know, I could go either with Option, Either or Try. Neither of those, however, seem as elegant as good old exceptions.

For example, dealing with the Try result would look like this (borrowed from similar question):

addUser(user) match {
   case Success(user) => Ok(user)
   case Failure(t: PSQLException) if(e.getSQLState == "23505") => InternalServerError("Some sort of unique key violation..")
   case Failure(t: PSQLException) => InternalServerError("Some sort of psql error..")
   case Failure(_) => InternalServerError("Something else happened.. it was bad..")
}

Those last two lines are exactly something I'd like to avoid because I'd have to add them anywhere I make a database query (and counting on MatchError doesn't seem like a good idea).

Also dealing with multiple error sources seems a bit cumbersome:

(for {
   u1 <- addUser(user1)
   u2 <- addUser(user2)
   u3 <- addUser(user3)
} yield {
  (u1, u2, u3)
}) match {
   case Success((u1, u2, u3)) => Ok(...)
   case Failure(...) => ...
}

How is that better than:

try {
     u1 = addUser(user1)
     u2 = addUser(user2)
     u3 = addUser(user3)
     Ok(...)
} catch {
     case (e: UserAlreadyExistsException) => ...
}

Has the former approach any advantages that I'm not aware of?

From what I understood, Try is very useful when passing exceptions between different threads but as long as I'm working within a single thread, it doesn't make much sense.

I'd like to hear some arguments and recommendations about this.

Community
  • 1
  • 1
tobik
  • 7,098
  • 7
  • 41
  • 53

1 Answers1

2

Much of this topic is of course a matter of opinion. Still, there are some concrete points that can be made:

  • You are correct to observe that Option, Either and Try are quite generic; the names do not provide much documentation. Therefore, you could consider a custom sealed trait:

    sealed trait UserAddResult
    case object UserAlreadyExists extends UserAddResult
    case class UserSuccessfullyAdded(user: User) extends UserAddResult
    

    This is functionally equivalent to an Option[User], with the added benefit of documentation.

  • Exceptions in Scala are always unchecked. Therefore, you would use them in the same cases you use unchecked exceptions in Java, and not for the cases where you would use checked exceptions.

  • There are monadic error handling mechanisms such as Try, scalaz's Validation, or the monadic projections of Either.

    The primary purpose of these monadic tools is to be used by the caller to organize and handle several exceptions together. Therefore, there is not much benefit, either in terms of documentation or behavior, to having your method return these types. Any caller who wants to use Try or Validation can convert your method's return type to their desired monadic form.

As you can maybe guess from the way I phrased these points, I favor defining custom sealed traits, as this provides the best self-documenting code. But, this is a matter of taste.

Owen
  • 38,836
  • 14
  • 95
  • 125
  • Thank you for your answer. So you would suggest that I replace checked exceptions with this "wrapped" return value and leave unchecked exceptions as they are, right? How would I handle several of these errors together? – tobik Jan 29 '16 at 08:36
  • @tobik That is a very good question but not one I feel prepared to answer right now. I encourage you to look into the documentation for `Validation` to see how to monadically handle errors. Though, in my opinion manually pattern matching on the results is not a bad solution. – Owen Feb 03 '16 at 22:25
  • Another problem is that handling errors using pattern matching heavily increases nesting level. I guess that it's to a certain extent an inevitable consequence of the functional approach... – tobik Feb 04 '16 at 10:32