I'd like to know what should be the signature of my methods so that I handle different kind of failures elegantly.
This question is somehow the summary of many questions I already had about error handling in Scala. You can find some questions here:
For now, I understand the following:
Repository layer
Now please consider that I have a UserRepository
. The UserRepository
stores the users and defines a findById
method. The following failures could happen:
OutOfMemoryError
)Additionally, the user could be missing, leading to an Option[User]
result
Using a JDBC implementation of the repository, SQL, non-fatal exceptions (constraint violation or others) can be thrown so it can make sense to use Try.
As we are dealing with IO operations, then the IO monad also makes sense if we want pure functions.
So the result type could be:
Try[Option[User]]
IO[Option[User]]
Service layer
Now let's introduce a business layer, UserService
, which provides some method updateUserName(id,newUserName)
that uses the previously defined findById
of the repository.
The following failures could happen:
Then the result type could be:
Try[Either[BusinessError,User]]
IO[Either[BusinessError,User]]
BusinessError here is not a Throwable because it is not an exceptional failure.
Using for-comprehensions
I would like to keep using for-comprehensions to combine method calls.
We can't easily mix different monads on a for-comprehension, so I guess I should have some kind of uniform return type for all my operations right?
I just wonder how do you succeed, in your real world Scala applications, to keep using for-comprehensions when different kind of failures can happen.
For now, for-comprehension works fine for me, using services and repositories which all return Either[Error,Result]
but all different kind of failures are melted together and it becomes kind of hacky to handle these failures.
Do you define implicit conversions between different kind of monads to be able to use for-comprehensions?
Do you define your own monads to handle failures?
By the way perhaps I'll be using an asynchronous IO driver soon.
So I guess my return type could be even more complicated: IO[Future[Either[BusinessError,User]]]
Any advice would be welcome because I don't really know what to use, while my application is not fancy: it is just an API where I should be able to make a distinction between business errors that can be shown to the client side, and technical errors. I try to find an elegant and pure solution.
In Error handling we have two possible paths either a computation succeeds or fails. The imperative way to control the flow is using exceptions and a try/catch block.
Rust doesn't want to have exceptions; it prefers return value based error handling ("monadic error handling", specifically). A lot of people seem to be under the impression that Rust's error handling model is a way of emulating exceptions -- it's not; it's a different model, and we don't want to emulate exceptions.
This is what Scalaz's EitherT
monad transformer is for. A stack of IO[Either[E, A]]
is equivalent to EitherT[IO, E, A]
, except that the former must be handled as multiple monads in sequence, whereas the latter is automatically a single monad that adds Either
capabilities to the base monad IO
. You can likewise use EitherT[Future, E, A]
to add non-exceptional error handling to asynchronous operations.
Monad transformers in general are the answer to a need to mix multiple monads in a single for
-comprehension and/or monadic operation.
EDIT:
I will assume you are using Scalaz version 7.0.0.
In order to use the EitherT
monad transformer on top of the IO
monad, you first need to import the relevant parts of Scalaz:
import scalaz._, scalaz.effect._
You also need to define your error types: RepositoryError
, BusinessError
, etc. This works as usual. You just need to make sure that you can, e.g., convert any RepositoryError
into a BusinessError
and then pattern match to recover the exact type of error.
Then the signatures of your methods become:
def findById(id: ID): EitherT[IO, RepositoryError, User]
def updateUserName(id: ID, newUserName: String): EitherT[IO, BusinessError, User]
Within each of your methods, you can use the EitherT
-and-IO
-based monad stack as a single, unified monad, available in for
-comprehensions as usual. EitherT
will take care of threading the base monad (in this case IO
) through the whole computation, while also handling errors the way Either
usually does (except already right-biased by default, so you don't have to constantly deal with all the usual .right
junk). When you want to do an IO
operation, all you have to do is raise it into the combined monad stack by using the liftIO
instance method on IO
.
As a side note, when working this way, the functions in the EitherT
companion object can be very useful.
Update 2 [2020-09]: there was some evolution in the scala ecosystem since that answer was first redacted. cat-effect 3
is talking about having a dedicated error channel [update 2021-03: it chose not to do so in the end], scalaz 8
stalled, and a new library emerged from it: ZIO
, a library which has at its core a bi-functor IO monad (+ a dependency injection system, out of the scope of present question), is gaining traction in scala.
As it has a dedicated error channel, just hit v1.0.0, and the topic is still novel, I answered the question (related to this one): What is ZIO error channel and how to get a feeling about what to put in it?.
It also deals with more general questions (like: discovering failure modes of application and dealing with them to let futur dev/adminsys/users have agency on behavior, even in case of errors) with a short summary to my talk systematic error management in application. Hope it helps and give more context to that (big and complex) topic.
The answer by @pthariens-flame is great, and you should just use it for your task at hand.
I would like to bring some contextual background of recent developments in the domain, so this is just a general information answer.
Error management is basically the #1 work of us, dev. The happy path is happy and boring, and it's not where the user will complain. Most (all?) the problems lies where effects (I/O in particular) are implied in the process.
One of the approach to manage thes problems is to follow what is commonly refered to as a "pure FP approch" where you draw a big red line between pure/total and impure/non-total parts of your programs. When doing so, you leverage the possibility to cleanly deal with errors depending of their kind.
In recent times (18 months?), Scala has seen a lot of research and development in that domain. Actually, I believe that Scala is today the most exciting and disrupting place in all languages on that very specific problem (but of course it's likely just a brain bias of availability/recent info).
Scalaz8, Monix and cats-effects are the 3 mains contributors of that fast evolution. So anything related to these 3 projects (conference talk, blog article, etc) will help you understand what is happening.
So, to keep the story short, Scalaz8 will change the way IO is model to better account of error management. John DeGoes is leading the effort here, and he produced some good resources on the subject:
Article:
Video:
There is also a lot of thing going on with Monix and Cats-effect, but I believe that most of the resources on the subject happen in pull requests in corresponding projects.
There is that talk by Alexandru Nedelcu which give some back ground of the problems:
And a comparison here by Adam Warski:
And finaly, there is excellent article by Luka Jacobowitz for the Cats part: "Rethinking MonadError" https://typelevel.org/blog/2018/04/13/rethinking-monaderror.html which covers a lot of the same ground with an other light.
[Edit]: as noticed by peers, the span of (r)evolution in the domain does not stop to that in scala-land. There is a big work done to try to make effects encoding (IO among other) more performant. The latest steps in the domain are trying to use Kleisli Arrows in place of monads to minimize GC churn on the JVM.
See:
Hope it helps!
Update [2018-07]: there was a long, interesting thread on the subject on reddit: "Can someone explain to me the benefits of IO?" https://www.reddit.com/r/scala/comments/8ygjcq/can_someone_explain_to_me_the_benefits_of_io/
And a contribution by John DeGoes: "Scala Wars: FP-OOP vs FP" http://degoes.net/articles/fpoop-vs-fp
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With