I asked this question in the Discord channel about ZIO 1.0.3:
If I have a ZIO[Any, Throwable, String] that just throws an Exception then I can handle the exception with ZIO.either.
But if I have a ZIO[Any, Throwable, String] that throws an Exception in the release of bracket I get:
Fiber failed.
An unchecked error was produced.
Is that expected? [Code to demonstrate here on GitHub]
Adam Fraser responded:
@PhillHenry The release action of bracket should never fail so it has a type of URIO[R, Any]. If there is an error there you need to expose the full cause and either handle it or ignore it.
You are kind of lying to the compiler by doing URIO(x.close()).
URIO promises that something will never fail but close can fail so that is going to create an unchecked exception.
If you make that something like Task(x.close()) to reflect the fact that the effect can fail then you are going to get a compilation error when you try to put that in bracket.
And then you have a choice as the user. If a finalizer fails, which really shouldn't happen, how do you want to treat it? One option is to say just fail at that point. Not successfully running the finalizer indicates a potential resource leak and better to fail fast than die slowly later. So then you would call orDie on the effect, and you could potentially handle the cause at a higher level of your application. Another option is to say we made our best efforts, we just need to move on, and then you would do ignore or maybe log the error somewhere and then ignore it.
Why couldn't the Exception manifest itself in the error channel of the ZIO?
Because the error channel models the failure of the acquire and use actions of bracket. It is possible that the use action fails and then the release action runs and also fails, so we need different channels for those errors.
Could ZIO not use Throwable.addSuppressed? I notice that this is what Java's try-with-resource does in these situations
Well we have to be polymotphic. The error of use may not be a Throwable at all so we definitely can't add a suppressed exception to that.
That also makes it really easy to lose failures in finalizers when normally a failure in a finalizer is very bad and something you should explicitly handle.
Well the only way you are cheating the compiler is by doing UIO(somethingThatThrows). I'm not sure how that can be prevented other than by preventing users from constructing UIO values at all, which prevents users from describing effects that really can't fail.
I think the lesson is just to only use UIO for effects that really don't throw exceptions (or if they do throw exceptions you are comfortable treating those as defects).
But we want to use bracket in a ton of situations where it is not.The bracket operator is core to safe resource management. Saying we could only use it when E was Throwable would prevent us from writing resource safe code with a polymorphic error type, which is basically all code we want to write.
The Cats way
Drew Boardman @drewboardman Feb 23 22:15Basically I'm trying to see if there exists something like MonadError that signals whether the error has been handled but I think this is only possible with datatypes, not with typeclasses. I was having a discussion about how MonadError doesn't really signal, at the type-level, that anything has been handled. I basically got around to just creating a datatype that signals this - but that effectively just re-invents Either
Adam Rosien @arosien Feb 23 22:18"whether the error has been handled" - do you mean knowing this statically? MonadError and the like don't distinguish, in the type, error-handling.
Fabio Labella @SystemFw Feb 23 22:18It's kinda possible with cats-mtl , but in general yeah it's a lot easier with datatypes... So in this case you'd have a MonadError constraint, and handle it (eliminate it) by instantiating to EitherT (locally) and then you handle that.
No comments:
Post a Comment