Repository navigation
Open
Description
GHc 9.12 introduced rethrowIO as well as catchNoPropagate and tryWithContext. The interesting part of rethrowIO is that:
- It does not introduces a new stack trace as exception annotation
- It does not clean the previous stack trace and exception annotations.
For example, the following code:
{-# LANGUAGE DeriveAnyClass #-}
import Control.Concurrent.Async
import Control.Exception
import Control.Exception.Context
import Control.Exception.Annotation
import Data.Typeable
import Data.Traversable
import GHC.Stack
data Ann = Ann String
deriving (Show, ExceptionAnnotation)
asyncTask :: HasCallStack => IO ()
asyncTask = annotateIO (Ann "bonjour") $ do
error "yoto"
asyncTask' :: HasCallStack => IO ()
asyncTask' = annotateIO (Ann "bonjour2") $ do
error "yutu"
main = do
-- withAsync asyncTask wait
concurrently asyncTask asyncTask'
-- race asyncTask asyncTask'Results in the following non informative output:
ASyncException.hs: Uncaught exception ghc-internal:GHC.Internal.Exception.ErrorCall:
yoto
HasCallStack backtrace:
throwIO, called at ./Control/Concurrent/Async/Internal.hs:630:24 in async-2.2.5-50rpfAJ7BEc1o5OswtTMUN:Control.Concurrent.Async.Internal
Especially:
- The backtrace points to the internal of async, completly losing the "real" and relevant backtrace
- The exception annoattion is lost
If run without the async "wrapping", we get instead:
ASyncException.hs: Uncaught exception ghc-internal:GHC.Internal.Exception.ErrorCall:
yoto
Ann "bonjour"
HasCallStack backtrace:
error, called at ASyncException.hs:15:3 in main:Main
asyncTask, called at ASyncException.hs:24:3 in main:Main
With annotation and backtrace.
This could be fixed by using rethrowIO instead of throw everywhere an exception is rethrown in async. See the attached MR which can be used as a proof of concept. A bit of discussion may be necessary to know if we want to add a WhileHandling context before rethrowing.
Reacted by Matthew Pickering
Metadata
Metadata
Assignees
Labels
No labels
Activity
mpickering commentedon Jan 8, 2026
It would be good to do a release which implements this change. You could get better backtraces when using
asyncif this was implemented.mpickering commentedon Jan 8, 2026
Note that there is the additional issue that
throwSTMdoes not attach backtrace information, in particularwaitSTMrethrows an exception usingthrowSTMso it gets no context at all.As a rule of thumb, anywhere an exception is caught and rethrown, use
rethrowIOrather thanthrowIO. We probably also needrethrowSTMfor this use case.guibou commentedon Jan 31, 2026
(Sorry, this was a comment intended for #165)
I'm a bit annoyed.
Take this code sample:
The current output is:
Here I'm annoyed by:
While handling, which is added by thewaitcall, but that's still unclear why to me.waitlocation, it only shows the callsite forasyncTask.I think the correct solution would be to add some sort of
While waiting atexception annotation onwait, adding anotherHasCallStackbacktrace for thewaitlocation ?simonmar commentedon Feb 3, 2026
I'm rather opposed to adding
HasCallStackanywhere in the core API. We already have a compile-time flagdebug-auto-labelthat addsHasCallStackand that's where I think it should stay.