Skip to content

Use displayException instead of show in top-level exception handler #198

Closed
@bgamari

Description

@bgamari

Tracking ticket: #164

When GHC's extensible exception mechanism was introduced, it did not provide the Exception class with a method to render exceptions to a string. Instead, GHC's implementation opted to use the show function. This introduced an uneasy tension:

  • We want error messages being displayed to the user to be easy-to-understand
  • We want the show to produce valid Haskell syntax (being the inverse of read)

In 2008 this tension was noticed and addressed with the introduction of the displayException method into Exception:

class (Show a, Typeable a) => Exception a where
    ...
    displayException :: a -> String
    displayException = show

However, at the time the CLC stopped short of changing the top-level exception handler (which handles exceptions not caught by the user) to use this function to display messages to the user.

Here I propose that we do precisely this. While this is a change in semantics, I argue that it is worthwhile as:

  • it only affects exceptions not otherwise caught by the user
  • it only affects output of stderr, which is already fragile at best
  • all exceptions defined by base use the default show implementation of displayException and therefore will not change in behavior

Furthermore, this change will facilitate showing of useful annotation information introduced by the exception backtrace proposal (section 2.10).

Implementation

This change will require changing GHC.Conc.Sync.uncaughtExceptionHandler as follows:

diff --git a/libraries/base/GHC/Conc/Sync.hs b/libraries/base/GHC/Conc/Sync.hs
index 7177c87dbf2..39b45c20a5a 100644
--- a/libraries/base/GHC/Conc/Sync.hs
+++ b/libraries/base/GHC/Conc/Sync.hs
@@ -950,9 +950,7 @@ uncaughtExceptionHandler = unsafePerformIO (newIORef defaultHandler)
       defaultHandler :: SomeException -> IO ()
       defaultHandler se@(SomeException ex) = do
          (hFlush stdout) `catchAny` (\ _ -> return ())
-         let msg = case cast ex of
-               Just Deadlock -> "no threads to run:  infinite loop or deadlock?"
-               _                  -> showsPrec 0 se ""
+         let msg = displayException se
          withCString "%s" $ \cfmt ->
           withCString msg $ \cmsg ->
             errorBelch cfmt cmsg

With a corresponding change to the Exception instance of GHC.IO.Exception.Deadlock to preserve existing behavior:

diff --git a/libraries/base/GHC/IO/Exception.hs b/libraries/base/GHC/IO/Exception.hs
index 5f8a1ca024a..c23c65680c1 100644
--- a/libraries/base/GHC/IO/Exception.hs
+++ b/libraries/base/GHC/IO/Exception.hs
@@ -100,7 +100,8 @@ blockedIndefinitelyOnSTM = toException BlockedIndefinitelyOnSTM
 data Deadlock = Deadlock
 
 -- | @since 4.1.0.0
-instance Exception Deadlock
+instance Exception Deadlock where
+    displayException _ = "no threads to run:  infinite loop or deadlock?"
 
 -- | @since 4.1.0.0
 instance Show Deadlock where

Migration

This change will result in different output on stderr in the case of uncaught exceptions. In general, users who want predictable output on stderr are urged to provide their own exception handler (either via setUncaughtExceptionHandler or a well-placed catch in their program). The current show-based output can be retained via:

setUncaughtExceptionHandler $ \(SomeException exc) -> do

To ease mitigation, we propose adding the following function exposing the current uncaught exception handler's behavior from GHC.Conc.Sync:

oldDefaultHandler :: SomeException -> IO ()
oldDefaultHandler = mkTopExceptionHandler $ \ se@(SomeException ex) ->
         case cast ex of
               Just Deadlock -> "no threads to run:  infinite loop or deadlock?"
               _             -> show se

Metadata

Metadata

Assignees

No one assigned

    Labels

    approvedApproved by CLC votebase-4.20Implemented in base-4.20 (GHC 9.10)

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions