-
Notifications
You must be signed in to change notification settings - Fork 51
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
catchSyncOrAsync
doesn't catch SyncExceptionWrapper
#104
Comments
catchSyncOrAsync
doesn't catch {A,}SyncExceptionWrapper
catchSyncOrAsync
doesn't catch SyncExceptionWrapper
I don't think there's any reason this function should unwrap |
oh, lol, i was doing some debugging and may have changed some behavior - i'll correct the output I think the odd thing to me is that the code does unwrap the |
OK, new test failure output:
And test suite code (with other examples, for symmetry): describe "catchSyncOrAsync" $ do
it "should catch sync exceptions" $ do
result <- (`catchSyncOrAsync` return) $ throwIO Exception1
result `shouldBe` Exception1
it "should catch async exceptions" $ do
result <- withAsyncExceptionThrown $ \m -> m `catchSyncOrAsync` return
result `shouldBe` cancelled
it "should catch unliftio-wrapped async exceptions" $ do
result <- withWrappedAsyncExceptionThrown $ \m -> m `catchSyncOrAsync` return
fromExceptionUnwrap result `shouldBe` Just Exception1
it "should catch unliftio-wrapped sync exceptions" $ do
result <- (`catchSyncOrAsync` return) $ throwIO Control.Exception.ThreadKilled
result `shouldBe` Control.Exception.ThreadKilled So it catches sync-thrown-as-sync exceptions, it catches async-thrown-as-async exceptions, it catches sync-thrown-as-async exceptions, and it fails to catch async-thrown-as-sync exceptions. ... Oh. The wrapped one is using The test suite Works if I do that: it "should catch unliftio-wrapped async exceptions" $ do
result <- withWrappedAsyncExceptionThrown $ \m -> m `catchSyncOrAsync` return
fromExceptionUnwrap result `shouldBe` Just Exception1
it "should catch unliftio-wrapped sync exceptions" $ do
result <- (`catchSyncOrAsync` return) $ throwIO Control.Exception.ThreadKilled
fromExceptionUnwrap result `shouldBe` Just Control.Exception.ThreadKilled 🤔 I think this behavior is a bit perplexing! Would you consider a patch that changes behavior, such that Here's my main motivation:
The I think the behavior I'm expecting is a bit like this: catch :: (Exception e) => IO a -> (e -> IO a) -> IO a
catch action handler =
action `EUnsafe.catch` \(someExn :: SomeException) ->
case fromException someExn of
Just e
| isSyncException e -> handler e
| otherwise ->
-- we caught an async exception without a wrapper
-- rewrap it
throwIO e
Nothing ->
-- is not a plain `e` - possibly a sync wrapped e?
case fromException someExn of
Just (SyncExceptionWrapper innerAsync) ->
case fromException innerAsync of
Just e ->
-- The user is trying to catch an async exception
-- The SyncExceptionWrapper indicates that it was thrown as a sync exception
-- Safe to handle
handler e
Nothing ->
-- We can rethrow here without rewrapping checks, because we're already in a SyncExceptionWrapper
EUnsafe.throwIO someExn
Nothing ->
-- The exception is not what we were expecting. toss it.
EUnsafe.throwIO someExn |
Maybe this is simply a difference in the implementation of the |
Hm - isSyncException (SyncExceptionWrapper ThreadKilled) = True
isSyncException ThreadKilled = False I think the confusing thing, to me, is that doing something like: action `catch` \(ExceptionInLinkedThread _ _) -> pure () won't ever catch Thrown from
|
What about this behavior? fdescribe "proposed catch" $ do
let
myCatch :: (MonadUnliftIO m, Exception e) => m a -> (e -> m a) -> m a
myCatch action handler =
withRunInIO $ \runInIO -> do
runInIO action `Control.Exception.catch` \someException@(SomeException inner) -> do
if isSyncException inner
then
case fromExceptionUnwrap someException of
Just e ->
runInIO $ handler e
Nothing ->
Control.Exception.throwIO someException
else
Control.Exception.throwIO someException
myHandle :: (MonadUnliftIO m, Exception e) => (e -> m a) -> m a -> m a
myHandle = flip myCatch
it "catches sync exceptions" $ do
myHandle (\Exception1 -> pure 'a') (throwIO Exception1)
`shouldReturn` 'a'
it "catches sync-wrapped async exceptions" $ do
myHandle (\ThreadKilled -> pure 'a') (throwIO ThreadKilled)
`shouldReturn` 'a'
it "does not catch async exceptions" $ do
myHandle (\ThreadKilled -> pure 'a') (Control.Exception.throwIO ThreadKilled)
`shouldThrow` (ThreadKilled ==)
it "does not catch async-wrapped sync exceptions" $ do
myHandle (\Exception1 -> pure 'a') (Control.Exception.throwIO (AsyncExceptionWrapper Exception1))
`shouldThrow` \(SomeException e) -> fromMaybe False $ do
SomeAsyncException e <- cast e
AsyncExceptionWrapper e <- cast e
Exception1 <- cast e
pure True |
I'm honestly not sure I'd be comfortable with any change to behavior here, even if it makes things more coherent. These kinds of semantics are subtle and not checked by the compiler. A change like this may silently break code in lots of different code bases. I think the best approach would be a new function providing the specific functionality you're looking for. But overall, my real thought is that it's a mistake to use the same type for both sync and async exceptions and try to make the catching work in any meaningful way. |
EDIT: the text/title of this issue are confused - I missed that the test that could see-through the wrapper was actually catching
SomeException
and doingfromExceptionUnwrap
on it.This test illustrates the issue:
This test currently fails:
We're doing
throwIO ThreadKilled
, which throwsSomeException (SyncExceptionWrapper ThreadKilled)
.Meanwhile,
catchSyncOrAsync
is not looking "through" theSyncExceptionWrapper
:The text was updated successfully, but these errors were encountered: