Annotate asynchronous exceptions
Opened this issue · 1 comments
Right now, none of the functions in this codebase do anything with asynchronous exceptions. This means you don't get any annotations! This sucks, and we should fix it.
However, we need to fix it such that async exceptions are annotated, and that the "asynchronicity" of that exception is not altered. For example, doing a checkpoint
must not change an exception from async ot sync such that a later catch
can handle it.
Well, this is harder than I thought it would be. There are multiple ways to throw async exceptions!
Control.Exception.throwTo
This calls toException :: Exception e => e -> SomeException
before delivering it to the target thread, so the target thread has a pretty simple SomeException
that blasts it down.
UnliftIO.Exception.throwTo
This calls toAsyncException
, which does a bit of a dance:
toAsyncException :: Exception e => e -> SomeException
toAsyncException e =
case fromException se of
Just (SomeAsyncException _) -> se
Nothing -> toException (AsyncExceptionWrapper e)
where
se = toException e
-- | Wrap up a synchronous exception to be treated as an asynchronous
-- exception.
--
-- This is intended to be created via 'toAsyncException'.
--
-- @since 0.1.0.0
data AsyncExceptionWrapper = forall e. Exception e => AsyncExceptionWrapper e
deriving Typeable
-- | @since 0.1.0.0
instance Show AsyncExceptionWrapper where
show (AsyncExceptionWrapper e) = show e
-- | @since 0.1.0.0
instance Exception AsyncExceptionWrapper where
toException = toException . SomeAsyncException
fromException se = do
SomeAsyncException e <- fromException se
cast e
#if MIN_VERSION_base(4,8,0)
displayException (AsyncExceptionWrapper e) = displayException e
#endif
So, an UnliftIO.Exception.throwTo tid Foo
will check to see if Foo
is an async exception. Foo
would "opt in" to being an async exception by using toAsyncException
in the Exception
instance, instead of the default. This would look like the AsyncException
instance, which uses toException = asyncExceptionToException = toException . SomeAsyncException
.
If the Exception
is not natively async, then it gets wrapped in UnliftIO.Exception.AsyncWrapper
.
If Foo
is normal, then the exception delivered to the thread is a SomeException (SomeAsyncException (UnliftIO.AsyncExceptionWrapper Foo))
.
Control.Exception.Safe.throwTo
This one is just like UnliftIO
, but it uses a different AsyncExceptionWrapper
type. So you need to handle these cases separately.
Unifying Approaches
It'd be really nice if unliftio
and safe-exceptions
used the same type. But currently neither depend on each other. Even when this is done, library authors will need to support both AsyncExceptionWrapper
s.
However, until then, it seems reasonable that annotated-exception
will need to be able to "see through" both kinds of AsyncExceptionWrapper
and handle things nicely.