core-libraries-committee icon indicating copy to clipboard operation
core-libraries-committee copied to clipboard

Evaluate backtraces for "error" exceptions at the moment they are thrown

Open mpickering opened this issue 1 week ago • 4 comments

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

mpickering avatar Jan 08 '26 16:01 mpickering