-
Notifications
You must be signed in to change notification settings - Fork 66
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Generalized exception handling #137
Comments
Currently, I'm returning tl;dr summary of below is that your proposal is a pragmatic one and hard to disagree with, but I'm curious how app authors should interact with libraries with produce It feels a bit weird to have Aff bake in the ExceptT behavior and therefore be a little less idiomatic than other PureScript code (only in that not everybody goes straight for That said, I find it hard to argue with being pragmatic here - Aff already has special handling to pipe the errors through nested Affs/Fibers. But it's also a bit weird - changing the error channel from its original purpose in Node of enabling throwing exceptions in async code to being a convenient place way to monadically handle expected failures. Unrecoverable errors aren't worth catching, so, perhaps you're right, the error channel should be used for monadic, general-purpose error-handling within an app. I wonder how this change would/should affect libraries which returns an A story about that: One reason I haven't liked Aff's current/previous error channel is that some web/app frameworks take an |
In idiomatic JavaScript, you only have exceptions, there's no monadic error handling.
There is no real semantic difference between attempt :: forall e a. Aff e a -> Aff Void (Either e a)
rethrow :: forall e a. Either e a -> Aff e a Since we can prove the absence of exceptions, the type of This pretty much necessitates a monomorphic catch :: forall e1 e2 a. (e1 -> Aff e2 a) -> Aff e1 a -> Aff e2 a This is really just a |
To clarify that, I was thinking of idiomatic async JavaScript, which uses an error-back and a callback. There, calling the error-back short-circuits the remaining calculations which produce the expected value, which feels monadic to me (where each imperative JS line becomes an |
Oh also, your examples of using |
I can certainly see the appeal of this but personally I'm not a huge fan; see #136 (comment). I think |
@MonoidMusician brought up a good point in Slack. |
@hdgarrood I don't think it's quite the same comparing the way IO uses exceptions to the way Aff uses exceptions. PS has all but completely shunned general exceptions except for interop. And as I pointed out above, there's no difference as far has the tediousness of handling errors between Edit: Again, there's no reason you can't continue to propagate |
Ok, yeah, fair point. I suppose I can just stick to |
I'm a big fan of this. Not only does it tremendously clarify semantics (automatically deciding many edge cases that were formerly under-constrained), but it enables types to be significantly more precise. I can require an |
Hi! What's the status of this? I'd really like to see this happen, is there something I can do to help? |
There is just quite a bit of work involved. One thing that really needs consideration is a compatibility story. |
This would be a fantastic improvement for type-safety. The only major drawback I can think of is unifying error types from 3rd-party libraries, and preventing changes in the number and types of the errors returned from being breaking changes necessarily. (Obviously, if you are branching off of a concrete error type in a 3rd-party library, it would be breaking for you if that type was no longer a part of the error return type of a function you are calling. But it wouldn't have to be breaking for people who let the error bubble-up to some top-level error handler.) That brings up another point, however. Which is that in order to do anything useful with an error in a top-level error handler, you have to know something about it. If |
Maybe polymorphic variants could be useful here. I’m gonna play around with it and see how it goes. |
Right now, exceptions in Aff use the underlying
Error
type ofEff
, which is represented by native JS exceptions. There's quite a bit of machinery under the hood for propagating these exceptions in an async manner for very little utility. In my use of Aff, I've found true exceptions to be rare, and because they are largely opaque on the PureScript-side (which I think is a good thing) there's not much to do by catching them. Since the machinery is already there, we should think about generalizing exceptions in Aff, rather than fixing it toError
. That is:How is this different than just using something like
ExceptT
? One, it is more efficient. Aff already has to propagateEither
s for results and error, so we already bake inExceptT
handling under the hood. Since we are already paying the cost of it, and not getting that much utility out of the current formulation, we might as well take advantage of it. Otherwise, it's largely a convenience. I don't think there's anything that can't be expressed by using an additionalExceptT
layer.What does this mean for the implementation? For that, I should clarify the three channels we use to propagate async state:
step
in the source code). This is just the normal happy path of Aff evaluation.fail
in the source code). This is used to propagate recoverable exceptions like withthrowError
.interrupt
in the source code). This is an irrecoverable panic, which is currently only used withkillFiber
.I'll also clarify the cases where we implicitly catch exceptions during the source of evaluation:
liftEff
. All lifted Effs always have exceptions caught regardless of whether it carries anexception
effect.makeAff
. We only catch theEff
which initiates the async effect.The
Error
type is currently used on both the error channel and the interrupt channel. There isn't any part of the implementation that assumes theError
type for the error channel, so the implementation would largely remain as it is. The question is what do we do about catching exceptions? We'll still catch them, but the only channel available to us (for arbitraryError
s) in this case would be the interrupt channel. This implies that an uncaught exception within a lifted Eff would result in a panic of the currentFiber
.Note that you can still propagate
Error
on the error channel, since it is parametric, but Aff would not implicitly catch and propagate anything on the error channel. A user would need to explicitly usecatchException
inliftEff
ormakeAff
. This does not limit any existing workflows.For forked Fibers which have panicked, any observers (through
joinFiber
) would necessarily need to panic as well. That is, panics are infectious. SincekillFiber
users the interrupt/panic channel, killing a Fiber would result in its observers also panicking.What about forked Fibers which have no observers (that is, nobody is waiting for it's result with
joinFiber
)? Currently, if a Fiber propagates an error or an interrupt, and no one is there to observe it, we rethrow it in a fresh, global stack usingsetTimeout
. This ensures that exceptions are still observable by things likewindow.onerror
or whatever node uses. However, if we have arbitrary user defined exceptions, we can't rethrow what's on the error channel since it isn't necessarily a instance ofError
(though we can still rethrow interrupts). There are a couple of possibilities:console.error
.Supervisor
API to observe uncaught exceptions (Extended supervisor API #132)We'd likely need to do both, unless we want to require that all Aff contexts have a
Supervisor
, which would make sense./cc @jdegoes @hdgarrood @chexxor
The text was updated successfully, but these errors were encountered: