Sitelet https://github.com/purescript-contrib/purescript-aff/issues/137
Skip to content

Generalized exception handling #137

Description

@natefaubion

Right now, exceptions in Aff use the underlying Error type of Eff, 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 to Error. That is:

data Aff e a
data Fiber e a

instance monadThrowAff :: MonadThrow e (Aff e)
instance monadErrorAff :: MonadError e (Aff e)
instance bifunctorAff :: Bifunctor Aff

How is this different than just using something like ExceptT? One, it is more efficient. Aff already has to propagate Eithers for results and error, so we already bake in ExceptT 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 additional ExceptT layer.

What does this mean for the implementation? For that, I should clarify the three channels we use to propagate async state:

  • The result channel (part of step in the source code). This is just the normal happy path of Aff evaluation.
  • The error channel (fail in the source code). This is used to propagate recoverable exceptions like with throwError.
  • The interrupt channel (interrupt in the source code). This is an irrecoverable panic, which is currently only used with killFiber.

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 an exception effect.
  • makeAff. We only catch the Eff 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 the Error 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 arbitrary Errors) 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 current Fiber.

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 use catchException in liftEff or makeAff. 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. Since killFiber 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 using setTimeout. This ensures that exceptions are still observable by things like window.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 of Error (though we can still rethrow interrupts). There are a couple of possibilities:

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions