Exception Annotations: Lay of the Land

Lobsters Hottest Tools

Summary

This technical blog post details the status and usage of exception annotations in GHC versions 9.10 to 10.0, covering backtrace types and debugging strategies for Haskell developers.

<p><a href="https://lobste.rs/s/slndvk/exception_annotations_lay_land">Comments</a></p>
Original Article Export to Word Export to PDF
View Cached Full Text

Cached at: 05/11/26, 01:02 PM

# Exception Annotations: Lay of the Land Source: [https://well-typed.com/blog/2026/05/lay-annotation-land/](https://well-typed.com/blog/2026/05/lay-annotation-land/) Exception annotations were introduced in GHC 9\.10, and can be an invaluable tool for debugging thorny problems\. The initial implementation had some important limitations that made them less useful in practice than one might hope, but fortunately the situation has since been much improved\. In this blog post we will give a detailed overview of the status quo as of GHC 9\.12/9\.14, identify some gotchas you should be aware and provide advise on how to deal with them, and briefly look ahead to what will change in GHC 10\.0\. We will also dedicate a section to discussing the problems in GHC 9\.10, for those who cannot yet upgrade\. Although we will recap all necessary definitions, this blog is*not*meant to be an introduction to exception annotations; if you have never used them before, you might first want to watch[The Haskell Unfolder Episode 29: exceptions, annotations and backtraces](https://www.youtube.com/watch?v=SwOkh9N41Y8&list=PLD8gywOEY4HaG5VSrKVnHxCptlJv2GAn7&index=30)\. ## Backtraces Before we look at the general framework for exception annotations, let’s first briefly recap the concept of*backtraces*, which is GHC’s answer to stack traces in other languages\. The situation is more complicated in Haskell due laziness, and there are actually four different kinds of backtraces: - based on`HasCallStack`annotations - based on cost\-centres \(which will require compiling your program with profiling enabled\) - based on[IPE info](https://well-typed.com/blog/2021/01/first-look-at-hi-profiling-mode/) - based on[DWARF info](https://well-typed.com/blog/2020/04/dwarf-1/) In this blog post we will use the first two only, but for the purposes of our main discussion here the choice actually does not matter much; see GHC proposal[Decorate exceptions with backtrace information](https://github.com/ghc-proposals/ghc-proposals/blob/master/proposals/0330-exception-backtraces.rst)for details\. If you’re interested in IPE backtraces specifically, you might also be interested in our blog post[Better Haskell stack traces via user annotations](https://well-typed.com/blog/2025/09/better-haskell-stack-traces/), which discusses some recent extensions we implemented to improve these\. ### `HasCallStack`backtraces Consider this simple Haskell program, where`main`calls`top`calls`middle`calls`bottom`: ``` bottom :: HasCallStack => IO () bottom = do bt <- collectBacktraces putStrLn $ displayBacktraces bt middle :: HasCallStack => IO () middle = bottom top :: HasCallStack => IO () top = middle main :: IO () main = top ``` A`HasCallStack`is essentially an additional function argument which is automatically populated by GHC at call sites with information about where the function was called\. When we run this program, we see something like this: ``` HasCallStack backtrace: collectBacktraces, called at exe/DemoCallStack.hs:13:11 in (..) bottom, called at exe/DemoCallStack.hs:18:10 in (..) middle, called at exe/DemoCallStack.hs:22:7 in (..) top, called at exe/DemoCallStack.hs:25:8 in (..) ``` The only thing worth noting here is that the moment a`HasCallStack`chain is broken, the backtrace is cut off there\. For example, if`middle`does not have a`HasCallStack`constraint, we can no longer see where`middle`was called from: ``` HasCallStack backtrace: collectBacktraces, called at exe/DemoCallStack.hs:19:11 in (..) bottom, called at exe/DemoCallStack.hs:24:10 in (..) ``` The fact that`top`still has a`HasCallStack`constraint does not matter: the callstack is cut at the first missing link\. ### Cost centre backtraces Cost centres are how GHC implements profiling: very roughly, the cost of a computation is attributed to its enclosing cost centre \(see chapter[Profiling](https://downloads.haskell.org/ghc/latest/docs/users_guide/profiling.html)of the GHC manual\)\. Like`HasCallStack`, this relies on source code annotations: ``` {-# SCC bottom #-} bottom :: HasCallStack => IO () bottom = do bt <- collectBacktraces putStrLn $ displayBacktraces bt {-# SCC middle #-} middle :: IO () middle = bottom {-# SCC top #-} top :: HasCallStack => IO () top = middle {-# SCC main #-} main :: IO () main = do setBacktraceMechanismState CostCentreBacktrace True top ``` Unlike`HasCallStack`, however, GHC offers ways for inserting such annotations automatically, which can often make cost centre based callstacks more practical than`HasCallStack`\. The most common flag to do this is`\-fprof\-auto`or \(in recent GHC\)`\-fprof\-late`\(see[Late Cost Centre Profiling](https://well-typed.com/blog/2023/03/prof-late/)\)\. This inserts cost centres around all top\-level functions, as we did manually above\. Cost centre backtraces must be explicitly enabled by calling[`setBacktraceMechanismState`](https://hackage-content.haskell.org/package/base-4.22.0.0/docs/Control-Exception-Backtrace.html#v:setBacktraceMechanismState%5D), and you need to compile your code with profiling enabled; the[`cabal`option`\-\-enable\-profiling`](https://cabal.readthedocs.io/en/stable/how-to-enable-profiling.html)both enables profiling as well as automatic cost centre insertion\. The backtrace for this example might look something like ``` Cost-centre stack backtrace: DemoCallStack.main (exe/DemoCallStack.hs:(32,1)-(34,7)) DemoCallStack.top (exe/DemoCallStack.hs:28:1-12) DemoCallStack.middle (exe/DemoCallStack.hs:24:1-15) DemoCallStack.bottom (exe/DemoCallStack.hs:(18,1)-(20,35)) ``` Be aware however that optimizations can delete cost centres, especially in simple examples like this \([\#27225](https://gitlab.haskell.org/ghc/ghc/-/issues/27225)\)\. ### Cost centres vs exception handling Consider the following example: as before,`main`calls`top`calls`middle`calls`bottom`, which prints a backtrace; however`bottom`then throws an excepton\. Meanwhile,`main`installs an exception handler called`handlerTop`, which in turn calls`handlerMiddle`calls`handlerBottom`, which prints its own backtrace: ``` bottom :: HasCallStack => IO () bottom = do bt <- collectBacktraces putStrLn $ displayBacktraces bt throwIO $ userError "Uhoh" middle :: HasCallStack => IO () middle = bottom top :: HasCallStack => IO () top = middle handlerBottom :: HasCallStack => SomeException -> IO () handlerBottom _e = do bt <- collectBacktraces putStrLn $ displayBacktraces bt handlerMiddle :: HasCallStack => SomeException -> IO () handlerMiddle e = handlerBottom e handlerTop :: HasCallStack => SomeException -> IO () handlerTop e = handlerMiddle e main :: IO () main = do setBacktraceMechanismState CostCentreBacktrace True top `catch` handlerTop ``` The`HasCallStack`backtrace printed by`bottom`is ``` HasCallStack backtrace: collectBacktraces, called at exe/DemoCCS.hs:24:11 in (..) bottom, called at exe/DemoCCS.hs:29:10 in (..) middle, called at exe/DemoCCS.hs:32:7 in (..) top, called at exe/DemoCCS.hs:41:5 in (..) ``` as before; the`HasCallStack`printed by`handlerBottom`is very similar: ``` HasCallStack backtrace: collectBacktraces, called at exe/DemoCCS.hs:13:11 in (..) handlerBottom, called at exe/DemoCCS.hs:17:19 in (..) handlerMiddle, called at exe/DemoCCS.hs:20:16 in (..) handlerTop, called at exe/DemoCCS.hs:41:18 in (..) ``` For the cost\-centre based backtrace, the one shown in`bottom`is as before: ``` Cost-centre stack backtrace: DemoCCS.main (exe/DemoCCS.hs:(39,1)-(41,27)) DemoCCS.top (exe/DemoCCS.hs:32:1-12) DemoCCS.middle (exe/DemoCCS.hs:29:1-15) DemoCCS.bottom (exe/DemoCCS.hs:(23,1)-(26,30)) ``` but the one shown in`handlerBottom`is more surprising: ``` Cost-centre stack backtrace: DemoCCS.main (exe/DemoCCS.hs:(39,1)-(41,27)) DemoCCS.top (exe/DemoCCS.hs:32:1-12) DemoCCS.middle (exe/DemoCCS.hs:29:1-15) DemoCCS.bottom (exe/DemoCCS.hs:(23,1)-(26,30)) DemoCCS.handlerTop (exe/DemoCCS.hs:20:1-30) DemoCCS.handlerMiddle (exe/DemoCCS.hs:17:1-33) DemoCCS.handlerBottom (exe/DemoCCS.hs:(12,1)-(14,35)) ``` Whether or not this is expected/correct behaviour is arguable, but the rule is this: the cost centre stack is not restored*until we leave the scope of`catch`*\. Put another way: the cost centre stack reflects the fact that`bottom`“calls”`handlerTop`, however indirectly\. This applies transitively: if`handlerTop`would throw an exception, which would then be caught by some other exception handler, then*its*backtrace would reflect that`top`“called”`handlerTop`“called” that other exception handler\. This kind of situation can arise quite naturally, for example when using handlers that deallocate some resources and then*rethrow*the exception\. ## Basic definitions Before we look at the subtleties that arise from actually catching and throwing \(or rethrowing\) exceptions, we’ll first get the basic definitions out of the way\. These have not changed much between recent GHC versions and are hopefully uncontroversial\. ### Exception annotations Exceptions annotations can basically be anything at all; the only requirement is that that we can display them: ``` class Typeable a => ExceptionAnnotation a where displayExceptionAnnotation :: a -> String ``` An important instance of this class is[`Backtraces`](https://hackage-content.haskell.org/package/base-4.22.0.0/docs/Control-Exception-Backtrace.html#t:Backtraces), which wraps a set of different kinds of backtraces: ``` instance ExceptionAnnotation Backtraces where displayExceptionAnnotation = Base.displayBacktraces ``` ### Exception context An exception context is essentially just a list of exception annotations\. However, since those annotations may be of different types, we need to wrap them in an existential: ``` data ExceptionContext = ExceptionContext [SomeExceptionAnnotation] data SomeExceptionAnnotation = forall a. ExceptionAnnotation a => SomeExceptionAnnotation a ``` There are functions for manipulating the exception context\. The most important are[`emptyExceptionContext`](https://hackage-content.haskell.org/package/base-4.22.0.0/docs/Control-Exception-Context.html#v:emptyExceptionContext)and[`addExceptionAnnotation`](https://hackage-content.haskell.org/package/base-4.22.0.0/docs/Control-Exception.html#v:addExceptionContext), for creating an empty context and inserting an annotation into an existing context respectively\. ``` emptyExceptionContext :: ExceptionContext addExceptionAnnotation :: ExceptionAnnotation a => a -> ExceptionContext -> ExceptionContext ``` ### Pivotal change:`SomeException` The pivotal change in all of this is in the definition of`SomeException`which, starting in GHC 9\.10, now has an associated list of annotations: ``` data SomeException = forall e. (Exception e, HasExceptionContext) => SomeException e type HasExceptionContext = (?exceptionContext :: ExceptionContext) ``` The use of an[implicit parameter](https://downloads.haskell.org/ghc/latest/docs/users_guide/exts/implicit_parameters.html#extension-ImplicitParams)means that pattern matching on`SomeException`remains possible in the same way as before \(though the annotations would be silently dropped\)\. There are various functions for extracting and manipulating the exception context associated with an exception, such as[`someExceptionContext`](https://hackage-content.haskell.org/package/base-4.22.0.0/docs/Control-Exception.html#v:someExceptionContext)and[`addExceptionContext`](https://hackage-content.haskell.org/package/base-4.22.0.0/docs/Control-Exception.html#v:addExceptionContext): ``` someExceptionContext :: SomeException -> ExceptionContext addExceptionContext :: ExceptionAnnotation a => a -> SomeException -> SomeException ``` However, probably the most important function for extending exception contexts is[`annotateIO`](https://hackage-content.haskell.org/package/base-4.22.0.0/docs/Control-Exception.html#v:annotateIO), which installs an exception handler that extends any exception that is thrown with the specified annotation: ``` annotateIO :: forall e a. ExceptionAnnotation e => e -> IO a -> IO a annotateIO ann (IO io) = IO (PrimOp.catch# io handler) where handler se = PrimOp.raiseIO# (addExceptionContext ann se) ``` It is important to emphasize that this is implemented with[primops](https://hackage-content.haskell.org/package/ghc-prim-0.13.0/docs/GHC-PrimopWrappers.html), not with the regular`catch`and`throwIO`functions, which do considerably more than merely catching and throwing, as we shall see\. ## `Exception`type class The`Exception`type class is a central abstraction in Haskell’s exception ecosystem\. As part of the exception annotation work, it has received one minor extension, and it was changed in two not\-so\-minor\-but\-rather\-subtle ways\. Let’s first get the part out of the way which has*not*changed: exceptions are no good if we cannot see them: ``` class (Typeable e, Show e) => Exception e where displayException :: e -> String displayException = show -- (..) ``` ### `backtraceDesired` The minor extension is a new function called[`backtraceDesired`](https://hackage-content.haskell.org/package/base-4.22.0.0/docs/Control-Exception.html#v:backtraceDesired), which indicates if a backtrace should be attached to exceptions of this type; we will see how this function is used when we discuss the[implementation of`throwIO`](https://well-typed.com/blog/2026/05/lay-annotation-land/#implementation)\. ``` class (Typeable e, Show e) => Exception e where -- (..) backtraceDesired :: e -> Bool backtraceDesired _ = True ``` The argument to`backtraceDesired`is already fully constructed exception; the question is whether a backtrace should be*added*to that exception\. In most cases the argument can simply be ignored, but it doesn’t*have*to be\. For all but a handful of specialized cases the default implementation \(indicating that yes, we want a backtrace\) will be fine\. ### `fromException` The not\-so\-minor\-but\-rather\-subtle changes are in`fromException`and`toException`, which remove and add the`SomeException`wrapper around exceptions respectively\. Let’s first look at`fromException`: ``` class (Typeable e, Show e) => Exception e where -- (..) fromException :: SomeException -> Maybe e fromException (SomeException e) = cast e ``` This may*look*no different from the implementation[prior to 9\.10](https://hackage.haskell.org/package/base-4.19.2.0/docs/src/GHC.Exception.Type.html#Exception), but recall that`SomeException`now has an additional field: the exception annotations\. As mentioned above, a pattern match like this will silently discard those annotations\. ### `toException` The final function in the`Exception`class is`toException`, which is intended to add the`SomeException`wrapper\. ``` class (Typeable e, Show e) => Exception e where -- (..) toException :: e -> SomeException ``` Prior to 9\.10, the default implementation literally just added the`SomeException`constructor: ``` -- implementation prior to 9.10 toException = SomeException ``` However, starting in 9\.10 we also need to give an initial value for the exception context\. The default implementation, reasonably enough, chooses the empty context: ``` -- implementation in 9.10, 9.12, 9.14, and 10.0 toException e = let ?exceptionContext = emptyExceptionContext in SomeException e ``` Unfortunately, however, the*documentation*of`toException`has also been modified, and now states that`toException`[should produce a`SomeException`with no attached`ExceptionContext`](https://hackage-content.haskell.org/package/base-4.22.0.0/docs/Control-Exception.html#v:toException)\. Personally, I think that is a mistake \([\#27194](https://gitlab.haskell.org/ghc/ghc/-/issues/27194)\); we will discuss this in the next session\. #### ⚠️ Caution: Instance for`SomeException`itself `SomeException`*itself*is also an instance of`Exception`;`fromException`is trivial, and`backtraceDesired`and`displayException`piggy\-back on the definition of whatever exception is wrapped: ``` instance Exception SomeException where fromException = Just backtraceDesired (SomeException e) = backtraceDesired e displayException (SomeException e) = displayException e -- (..) ``` The definition of`toException`is more problematic, however\. Prior to 9\.10, calling`toException`on`SomeException`was just an identity: ``` instance Exception SomeException where -- (..) -- Prior to 9.10 toException se = se ``` Now, however, the implementation must*clear*the existing context in order to satisfy the contract: ``` instance Exception SomeException where -- (..) toException (SomeException e) = let ?exceptionContext = emptyExceptionContext in SomeException e ``` I think this is simply wrong; at the very least, it is highly counter\-intuitive, and it also does not match[the original proposal](https://github.com/ghc-proposals/ghc-proposals/blob/master/proposals/0330-exception-backtraces.rst); I don’t know why this was changed\. We will see some[consequences of this design choice](https://well-typed.com/blog/2026/05/lay-annotation-land/#caution-throwing-someexception)when we discuss throwing exceptions\. ### Newtype helpers There are two auxiliary types, with their own`Exception`instances, that can be helpful when throwing or catching exceptions in specific ways\. We haven’t discussed either throwing or catching yet, but we will nonetheless discuss these auxiliary types first as we will need them in the subsequent sessions\. #### `NoBacktrace` `NoBacktrace`can be used to override`backtraceDesired`: ``` newtype NoBacktrace e = NoBacktrace e instance Exception e => Exception (NoBacktrace e) where fromException = fmap NoBacktrace . fromException toException (NoBacktrace e) = toException e backtraceDesired _ = False -- displayException left at its default implementation ``` #### `ExceptionWithContext` The other, arguably more imporant, auxiliary type is`ExceptionWithContext`\. The definition itself is straight\-forward: it simply pairs some value with an exception context: ``` data ExceptionWithContext a = ExceptionWithContext ExceptionContext a ``` The idea is that this type gives us a way to catch exceptions of specific types \(rather than catching`SomeException`\), and still get access to the exception context\. For example: ``` data MyException = MyException deriving stock (Show) deriving anyclass (Exception) example :: IO () example = someAction `catch` \(ExceptionWithContext ctxt MyException) -> do -- (..) ``` The implementation is reasonably straight\-forward: ``` instance Exception a => Exception (ExceptionWithContext a) where toException (ExceptionWithContext ctxt e) = case toException e of SomeException c -> let ?exceptionContext = ctxt in SomeException c fromException se = do e <- fromException se return (ExceptionWithContext (someExceptionContext se) e) backtraceDesired (ExceptionWithContext _ e) = backtraceDesired e displayException = displayException . toException ``` That said, the devil is very much in the detail with these kinds of definitions, and as we shall see, it was[defined incorrectly in GHC 9\.10](https://well-typed.com/blog/2026/05/lay-annotation-land/#duplicated-annotations)\. ## Throw The primary function for throwing an exception is`throwIO`, which is defined as[1](https://well-typed.com/blog/2026/05/lay-annotation-land/#fn1) ``` throwIO :: (HasCallStack, Exception e) => e -> IO a throwIO e = do se <- toExceptionWithBacktrace e IO (PrimOp.raiseIO# se) ``` Most of the actual work happens in`toExceptionWithBacktrace`: ``` toExceptionWithBacktrace :: (HasCallStack, Exception e) => e -> IO SomeException toExceptionWithBacktrace e = if backtraceDesired e then do bt <- Base.collectBacktraces return (addExceptionContext bt (toException e)) else return (toException e) ``` That is, if a backtrace is desired, we collect one and add it as an annotation to the exception that we’re about to throw\. ### Generalization In GHC 9\.14`toExceptionWithBacktrace`was generalized to ``` toExceptionWithBacktrace :: (HasCallStack, Exception e) => e -> IO SomeException toExceptionWithBacktrace e = if backtraceDesired e then do SomeExceptionAnnotation ea <- collectExceptionAnnotation return (addExceptionContext ea (toException e)) else return (toException e) ``` This is an experimental API \(not yet part of`base`\); see[CLC \#348](https://github.com/haskell/core-libraries-committee/issues/348)for details\. The idea is that you can use[`setCollectExceptionAnnotation`](https://hackage-content.haskell.org/package/ghc-internal-9.1401.0/docs/GHC-Internal-Exception-Backtrace.html#v:setCollectExceptionAnnotation)to register your own function to be run to construct an annotation whenever an exception is thrown anywhere\. For example, if you’re worried that some IO faults are happening due to your CPU overheating, you might use ``` newtype Temperature = Temperature Int deriving stock (Show) deriving anyclass (ExceptionAnnotation) getTempCPU :: IO Temperature getTempCPU = -- (..) main :: IO () main = do setCollectExceptionAnnotation getTempCPU -- (..) ``` By default, the collection callback is`collectBacktraces`, so unless you register a different callback the behaviour is the same as in 9\.10 and 9\.12\. ### ⚠️ Caution: Throwing`SomeException` Because`throwIO`calls`toException`, and since`toException`for`SomeException`[*clears the exception context*](https://well-typed.com/blog/2026/05/lay-annotation-land/#instance-for-someexception-itself), you probably don’t want to call`throwIO`on an argument of type`SomeException`: any exception annotations that might be embedded in that exception will be lost\. The most common case for throwing`SomeException`is*inside*an exception handler; we will cover this specific case of*rethrowing*exceptions[when we discuss`onException`](https://well-typed.com/blog/2026/05/lay-annotation-land/#caution-rethrowing-the-same-exception), but we can reuse the same combinators also to define a general “throw precisely this exception” function: ``` raiseIO :: SomeException -> IO () raiseIO (SomeException e) = rethrowIO (ExceptionWithContext ?exceptionContext e) ``` ## Catch The most important change in GHC 9\.12 from 9\.10 is in the definition of`catch`, which now implements the[`WhileHandling`](https://github.com/haskell/core-libraries-committee/issues/202)proposal\. The idea is that when we throw a new exception while handling another, we annotate that new exception*with*the old exception: the new exception arose*while handling*the old exception: ``` data WhileHandling = WhileHandling SomeException deriving Show catch :: Exception e => IO a -> (e -> IO a) -> IO a catch (IO io) handler = IO $ PrimOp.catch# io handler' where handler' se = case fromException se of Just e' -> PrimOp.catch# (unIO (handler e')) (handler'' se) Nothing -> PrimOp.raiseIO# se handler'' se se' = PrimOp.raiseIO# (addExceptionContext (WhileHandling se) se') ``` ### ⚠️ Caution: Rethrowing the same exception An important combinator for dealing with exceptions is`onException`, which runs some specified action when an exception occurs \(typically some resource cleanup\) and then rethrows the exception again: ``` onException :: IO a -> IO b -> IO a onException io what = io `catch` \e -> do _ <- what throwIO (e :: SomeException) ``` As written, this is suboptimal: for every layer of`onException`, we re\-throw the annotation stripped from its original annotations \(due to`throwIO`and`toException`for`SomeException`\), and with a new`WhileHandling`annotation with the original exception \(due to`catch`\)\. This result in unnecessary noise: all the information is still there, but it’s buried\. When we*rethrow*the same exception, there is no need for`WhileHanding`: we should just throw the original exception as\-is\. To solve this,`base`now offers new functions specifically to catch\-and\-rethrow:`catchNoPropagate`[2](https://well-typed.com/blog/2026/05/lay-annotation-land/#fn2)is like the old`catch`, without the handler that adds the`WhileHandling`annotation; and`rethrowIO`, which avoids adding a backtrace \(using[`NoBacktrace`](https://well-typed.com/blog/2026/05/lay-annotation-land/#nobacktrace); moreover, both of these explicitly preserve contexts \(using[`ExceptionWithContext`](https://well-typed.com/blog/exceptionwithcontext)\): ``` catchNoPropagate :: Exception e => IO a -> (ExceptionWithContext e -> IO a) -> IO a catchNoPropagate (IO io) handler = IO $ PrimOp.catch# io handler' where handler' se = case fromException se of Just e' -> unIO (handler e') Nothing -> PrimOp.raiseIO# se rethrowIO :: Exception e => ExceptionWithContext e -> IO a rethrowIO e = throwIO (NoBacktrace e) ``` This then enables the following improved implementation of`onException`: ``` onException :: IO a -> IO b -> IO a onException io what = io `catchNoPropagate` \e -> do _ <- what rethrowIO (e :: ExceptionWithContext SomeException) ``` ## ⚠️ Caution: Displaying exceptions The final pitfall we need to discuss is*displaying*exceptions\. Usually we call`displayException`to do so, but this does*not*show annotations\. The idea is that`displayException`is meant to render an exception for*users*, not necessarily*developers*\.[3](https://well-typed.com/blog/2026/05/lay-annotation-land/#fn3)Starting withGHC 9\.14 there is a separate function[`displayExceptionWithInfo`](https://hackage-content.haskell.org/package/base-4.22.0.0/docs/Control-Exception.html#v:displayExceptionWithInfo), but that is not available in GHC 9\.12; moreover, even in GHC 9\.14 I would advise against using it when you are debugging, as*it only shows the top\-level annotations*, making things like[`WhileHandling`](https://well-typed.com/blog/2026/05/lay-annotation-land/#catch)much less useful\. Personally, I like to use my own custom exception handler which shows the full exception, and makes a few other improvements also: it makes the nesting structure clearer, and reorders annotations to improve readability; you can find an example implementation[on GitHub](https://gist.github.com/edsko/49cc535d712048f6cac532e8a02ea374)\. ## GHC 9\.10 If you cannot upgrade from GHC 9\.10, unfortunately the exception annotation infrastructure has some important limitations\. Upgrade if you can; if not, this section will explain what you need to be aware of\. ### Lost annotations As we remarked[when we discussed`catch`](https://well-typed.com/blog/2026/05/lay-annotation-land/#catch), the`WhileHandling`proposal only got implemented in GHC 9\.12\. In GHC 9\.10 the definition of`catch`was still unchanged from its definition before the exception annotation proposal: ``` catch :: Exception e => IO a -> (e -> IO a) -> IO a catch (IO io) handler = IO $ PrimOp.catch# io handler' where handler' se = case fromException se of Just e' -> unIO (handler e') Nothing -> PrimOp.raiseIO# se ``` However, the[`Exception`instance for`SomeException`](https://well-typed.com/blog/2026/05/lay-annotation-land/#caution-instance-for-someexception-itself)*was*already changed, so that`toException`clears the exception context\. This means that if an exception with annotations is*ever*caught and rethrown anywhere, in a pattern such as ``` someAction `catch` \(e :: SomeException) -> throwIO e ``` those annotations will be lost\. Similarly, since`onException`had not yet been changed either, any call to`onException`, and by implification`bracket`, anywhere in your callstack would also lose any annotations: ``` bracket :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c bracket before after thing = mask $ \restore -> do a <- before r <- restore (thing a) `onException` after a _ <- after a return r onException :: IO a -> IO b -> IO a onException io what = io `catch` \e -> do _ <- what throwIO (e :: SomeException) ``` In both cases,`throwIO`*will*insert a new backtrace, but that backtrace will point to where the exception was*rethrown*, not to where it was thrown originally\. What’s worse, neither`bracket`nor`onException`have a`HasCallStack`constraint, so all we see in the callstack is the call to`throwIO`from`onException`itself\. Cost centre stacks do help a bit here \(provided you enable profiling\): at least you’ll get to see the full backtrace to the exception handler, and with a bit of luck even to the original call to throw, due to the semantics of[semantics of cost centres in exception handlers](https://well-typed.com/blog/2026/05/lay-annotation-land/#cost-centres-vs-exception-handling)\. That won’t always be the case though \(for example, in the case of asynchronous exceptions\), and you won’t see any of the additional annotations that might have been added to the exception\. ### Duplicated annotations The`Exception`instance for`ExceptionWithContext`in GHC 9\.10 has an incorrect definition for`toException`: ``` instance Exception a => Exception (ExceptionWithContext a) where -- (..) -- implementation in GHC 9.10 toException (ExceptionWithContext ctxt e) = let ?exceptionContext = ctxt in SomeException e ``` \(We saw[the correct definition](https://well-typed.com/blog/2026/05/lay-annotation-land/#exceptionwithcontext)above\.\) This is wrong for two reasons: 1. It does not use`toException`of the underlying type \(the`a`type parameter\); in*most*cases this does not matter, because`toException`rarely does anything interesting\. Even in the case of`SomeException`, where`toException`does something “interesting” \(if perhaps ill\-advised\), to wit clear the exception context, that doesn’t matter here because we are overriding that context*anyway*\. However, there*might*be types where`toException`genuinely does something important \(even if I am not aware of any such cases currently\)\. 2. In the specific case that`a`*is*`SomeException`, this will create a*nested*`SomeException`:`SomeException \(SomeException someOtherException\)`with two copies of the context \(the annotations\)\. The second point here is more important: if we later have exception handlers that manipulate the exception context, they will manipulate the outer context but not the inner\. Indeed, if that “manipulation” is “*clear*the context” \(see previous section\), we might end up in the somewhat bizarre situation where these two problems cancel out: if we have ``` someAction `catch` \(ExceptionWithContext ctxt (e :: SomeException)) throwIO $ ExceptionWithContext ctxt e ``` then this exception handler will duplicate the annotations, a later exception handler might lose the outermost annotations \(previous section\) but not the inner, and all of a sudden annotations that were lost mysteriously re\-appear; see GHC ticket[\#27194](https://gitlab.haskell.org/ghc/ghc/-/issues/27194)\. Unfortunately, this is not a viable workaround for the lost annotation problem, as it changes the type of the exception nested in the \(outer\)`SomeException`from whatever it really should have been to \(the inner\)`SomeException`, which will break any exception handlers for that specific type\. ## GHC 10\.0 The upcoming GHC 10\.0 releases makes a few improvements to the exception annotation infrastructure\. The first important improvement is that exception handling in*STM*was lagging behind a bit; this will be rectified \([\#25365](https://gitlab.haskell.org/ghc/ghc/-/issues/25365)\)\. The other important fix is in`onException`\. Consider again the definition we saw[when we discussed rethrowing exceptions](https://well-typed.com/blog/2026/05/lay-annotation-land/#caution-rethrowing-the-same-exception): ``` onException :: IO a -> IO b -> IO a onException io what = io `catchNoPropagate` \e -> do _ <- what rethrowIO (e :: ExceptionWithContext SomeException) ``` We mentioned that that`catchNoPropagate`does*not*install an exception handler that installs a`WhileHandling`annotation, because we are rethrowing the very same exception\. However, if`what`throws an exception that is no longer the case\! The definition of`onException`is therefore modified to ``` onException io what = io `catchNoPropagate` \e -> do _ <- annotateIO (whileHandling e) what rethrowIO (e :: ExceptionWithContext SomeException) ``` See[CLC Proposal \#397](https://github.com/haskell/core-libraries-committee/issues/397)for details\. As an example, consider what happens if the release callback of`bracket`itself throws an exception: ``` data ReleaseFailed = ReleaseFailed deriving stock (Show) deriving anyclass (Exception) bottom :: HasCallStack => IO () bottom = annotateIO (MyAnnotation 123456789) $ throwIO MyException middle :: HasCallStack => IO () middle = bracket (return ()) (\() -> throwIO ReleaseFailed) $ \() -> bottom top :: HasCallStack => IO () top = middle ``` With the new definition`onException`\(and my custom exception display function, which is still needed\), we get ``` demo-bracket-release-fail: Uncaught exception of type ReleaseFailed ReleaseFailed HasCallStack backtrace: throwIO, called at exe/DemoBracketReleaseFail.hs:42:38 in (..) middle, called at exe/DemoBracketReleaseFail.hs:46:7 in (..) top, called at exe/DemoBracketReleaseFail.hs:55:5 in (..) WhileHandling MyException MyException MyAnnotation 123456789 HasCallStack backtrace: throwIO, called at exe/DemoBracketReleaseFail.hs:38:48 in (..) bottom, called at exe/DemoBracketReleaseFail.hs:42:70 in (..) middle, called at exe/DemoBracketReleaseFail.hs:46:7 in (..) top, called at exe/DemoBracketReleaseFail.hs:55:5 in (..) ``` Very nice\! ## Conclusions Exception annotations can be invaluable when debugging difficult problems\. While the initial implementation in GHC 9\.10 had some important limitations, the situation has since been much improved\. Provided you use GHC 9\.12 or later, there are two things to pay attention to in your own code \(these apply to 9\.12, 9\.14 and 10\.0\): - Define your own custom function to display exceptions, which shows*all*annotations, not just the top\-level ones \(or use[mine](https://gist.github.com/edsko/49cc535d712048f6cac532e8a02ea374)\)\. - Be cautious with throwing`SomeException`:`toException`for`SomeException`will clear the exception context, which is almost certainly not what you want\. For catch\-and\-rethrow, use the[combinators available specifically for that purpose](https://well-typed.com/blog/2026/05/lay-annotation-land/#caution-rethrowing-the-same-exception)\. That said, there are still a few minor shortcomings to be aware of: - **GHC 9\.12 and 9\.14**: - Exception handling*in STM*has not yet been updated:`throwSTM`does not collect a backtrace, and`catchSTM`does not add any`WhileHandling`annotations \([\#25365](https://gitlab.haskell.org/ghc/ghc/-/issues/25365)\)\. - `onException`does not add any`WhileHandling`exceptions; as a result, if the resource deallocation callback to`bracket`*itself*throws an exception, the original exception will be lost\. Both of these will be addressed in GHC 10\.0\. - **exceptions\-0\.10\.9**: this is the version of`exceptions`that is bundled with GHC 9\.12, but lags behind a bit\. For example, the definition of`generalBracket`in[`exceptions\-0\.10\.9`](https://hackage.haskell.org/package/exceptions-0.10.9/docs/src/Control.Monad.Catch.html#generalBracket)does not use any of the abstractions for rethrowing; this is fixed in[`exceptions\-0\.10\.12`](https://hackage-content.haskell.org/package/exceptions-0.10.12/docs/src/Control.Monad.Catch.html#generalBracket)\. The impact is however limited: it merely means that there are some extraneous`WhileHandling`annotations, resulting in unnecessary noise\. - Any catch\-and\-rethrow patterns implemented in other packages*should*not lose any annotations, provided that they use`catch`from`base`\. --- 1. We will ignore calls to`withFrozenCallStack`, which hide some internal functions from the`HasCallStack`backtrace\. This makes the backtrace slightly more readable, but does not otherwise change anything\. See[CLC \#387](https://github.com/haskell/core-libraries-committee/issues/387)\.[↩︎](https://well-typed.com/blog/2026/05/lay-annotation-land/#fnref1) 2. Some versions of`base`distinguish between`catchExceptionNoPropagate`and`catchNoPropagate`, which differ only in some strictness annotations\. Strictness can make a big difference, especially when IO actions are`undefined`rather than throwing an exception\. However, this is its own can of worms, and outside the scope of this blog post\. See[CLC proposal \#383](https://github.com/haskell/core-libraries-committee/issues/383)for some discussion\.[↩︎](https://well-typed.com/blog/2026/05/lay-annotation-land/#fnref2) 3. In GHC 9\.10,`displayException`*did*show annotaitons, but this got rolled back in 9\.12; see[CLC \#285](https://github.com/haskell/core-libraries-committee/issues/285)for a detailed discussion\.[↩︎](https://well-typed.com/blog/2026/05/lay-annotation-land/#fnref3)

Similar Articles

Fighting Hyrum's Law in LLVM

Lobsters Hottest

This article surveys mechanisms within the LLVM compiler infrastructure designed to prevent dependencies on unspecified behavior, known as Hyrum's Law, to ensure build reproducibility.

Understanding Annotator Safety Policy with Interpretability

arXiv cs.AI

This paper introduces Annotator Policy Models (APMs) by Apple, which use interpretability techniques to infer annotators' internal safety policies from their labeling behavior without requiring additional annotation effort. The authors demonstrate that APMs can accurately model these policies and distinguish between sources of annotation disagreement, such as operational failures, policy ambiguity, and value pluralism.

LACE: Lattice Attention for Cross-thread Exploration

arXiv cs.AI

LACE introduces a lattice attention mechanism that enables concurrent reasoning paths in LLMs to share intermediate insights and correct errors during inference, improving reasoning accuracy by over 7 points compared to standard isolated parallel sampling.