core-libraries-committee
core-libraries-committee copied to clipboard
Evaluate backtraces for "error" exceptions at the moment they are thrown
If we look at the definition of throw you will see that the SomeException is forced before raise# is called.
77 -- | Throw an exception. Exceptions may be thrown from purely
78 -- functional code, but may only be caught within the 'IO' monad.
79 --
80 -- WARNING: You may want to use 'throwIO' instead so that your pure code
81 -- stays exception-free.
82 throw :: forall (r :: RuntimeRep). forall (a :: TYPE r). forall e.
83 (HasCallStack, Exception e) => e -> a
84 throw e =
85 -- Note the absolutely crucial bang "!" on this binding!
86 -- See Note [Capturing the backtrace in throw]
87 -- Note also the absolutely crucial `noinine` in the RHS!
88 -- See Note [Hiding precise exception signature in throw]
89 let se :: SomeException
90 !se = noinline (unsafePerformIO (toExceptionWithBacktrace e))
91 in raise# se
The notes [Capturing the backtrace in throw] and [Hiding precise exception signature in throw] explain the implementation.
However, error calls raise# directly, which bypasses this important bang in throw.
33 -- | 'error' stops execution and displays an error message.
34 error :: forall (r :: RuntimeRep). forall (a :: TYPE r).
35 HasCallStack => [Char] -> a
36 error s = raise# (errorCallWithCallStackException s ?callStack)
199 errorCallWithCallStackException :: String -> CallStack -> SomeException
200 errorCallWithCallStackException s stk = unsafeDupablePerformIO $ do
201 withFrozenCallStack $ toExceptionWithBacktrace (ErrorCall s)
202 where ?callStack = stk
The result is that exceptions raised by error don't have IPE backtraces which point to the correct location, since the IPE backtrace is only collected when the exception context is forced.
Proposal: Implement error and undefined like throw to get accurate IPE backtraces.
Implementation: https://gitlab.haskell.org/ghc/ghc/-/merge_requests/15306