# Exception Annotations: Lay of the Land

**URL:** https://discourse.haskell.org/t/exception-annotations-lay-of-the-land/14056
**Category:** Uncategorized
**Created:** [May 8, 2026, 3:06pm UTC](https://discourse.haskell.org/t/exception-annotations-lay-of-the-land/14056 "2026-05-08T15:06:38Z")
**Posts on this page:** 8
**Page:** 1

<div class="post-metadata">

### Author: ![edsko](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/edsko/32/3024_2.png) [@edsko](https://discourse.haskell.org/u/edsko)
#### Post date: [May 8, 2026, 3:06pm UTC](https://discourse.haskell.org/t/exception-annotations-lay-of-the-land/14056/1 "2026-05-08T15:06:39Z")

</div>

> **[Exception Annotations: Lay of the Land](https://well-typed.com/blog/2026/05/lay-annotation-land/)**

New blog post which gives a comprehensive overview of the exception annotation infrastructure, which has been much improved in GHC 9.12, and discuss some things to be aware of. TL;DR: Use ghc 9.12, don’t call `throwIO` on an argument of type `SomeException`, and use your own function instead of `displayException` if you want to see all exception annotations.

---

<div class="post-metadata">

### Author: ![danidiaz](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/danidiaz/32/92_2.png) [@danidiaz](https://discourse.haskell.org/u/danidiaz)
#### Post date: [May 9, 2026, 9:22am UTC](https://discourse.haskell.org/t/exception-annotations-lay-of-the-land/14056/2 "2026-05-09T09:22:16Z")

</div>

Great overview, thanks!

About the [issue](https://gitlab.haskell.org/ghc/ghc/-/issues/27194) of whether, in the `Exception` instance for `SomeException`, the definition of `toException` should clear the context.

If it clears the context (as it does currently) this code

```haskell
catchAndThrow :: IO a -> IO a
catchAndThrow = handle $ \(e :: SomeException) -> throwIO e

```

will lose the existing anotations in `e`, instead adding backtrace information (say, an `IPE` backtrace) corresponding to the `throwIO`, correct?

If we modify `SomeException`’s `toException` to keep the context, wouldn’t that same code result in _two_ unrelated backtraces, one for the original exception, another for the `throwIO` call site? 🤔

---

<div class="post-metadata">

### Author: ![danidiaz](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/danidiaz/32/92_2.png) [@danidiaz](https://discourse.haskell.org/u/danidiaz)
#### Post date: [May 10, 2026, 8:45am UTC](https://discourse.haskell.org/t/exception-annotations-lay-of-the-land/14056/3 "2026-05-10T08:45:26Z")

</div>

At the risk of confusing myself and others 😅 (this is full of subtle issues!)

Suppose we made the `toException` of _both_ `SomeException` and `ExceptionWithContext` actually respect the “produce a `SomeException` with no attached `ExceptionContext`” invariant. No exceptions (pun accidental) for it!

Suppose as well that we added a separate `toExceptionPreservingContext :: ExceptionWithContext e -> SomeException` function, and also redefined `rethrowIO` as

```haskell
rethrowIO :: Exception e => ExceptionWithContext e -> IO a
rethrowIO e = throwIO (NoBacktrace (toExceptionPreservingContext e))

```

Would that work / be better? 🤔 Instance definitions would be more uniform, at least.

---

<div class="post-metadata">

### Author: ![edsko](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/edsko/32/3024_2.png) [@edsko](https://discourse.haskell.org/u/edsko)
#### Post date: [May 13, 2026, 7:58am UTC](https://discourse.haskell.org/t/exception-annotations-lay-of-the-land/14056/4 "2026-05-13T07:58:14Z")

</div>

I don’t know what the original motivation was for clearing the context of `SomeException`; if someone does, please do let me know, I’d love to know. Now that we are here though, I hesitate to argue that we should stop doing that; there are a lot of puzzle pieces here that need to fit together _just_ so, and it’s difficult to be sure what happens when we change something quite so central as `SomeException` itself. Your `toExceptionPreservingContext` proposal at least separates out these concerns; if we conclude that the behaviour of `toException` for `SomeException` _is_ the right one, then perhaps something like that might be useful. It would still be quite confusing though that `throwIO (ExceptionWithContext ctxt e)` would not actually use that context `ctxt` though! Personally I think we _should_ change the behaviour and simply drop the requirement that the context must be empty, but it’s hard to be sure.

As for your question about the two backtraces: yes, you are correct. That’s why we have `catchNoPropagate` and `rethrowIO`.

---

<div class="post-metadata">

### Author: ![danidiaz](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/danidiaz/32/92_2.png) [@danidiaz](https://discourse.haskell.org/u/danidiaz)
#### Post date: [May 16, 2026, 8:48am UTC](https://discourse.haskell.org/t/exception-annotations-lay-of-the-land/14056/5 "2026-05-16T08:48:11Z")

</div>

I don’t know why the invariant that [`toException`](https://hackage-content.haskell.org/package/base-4.22.0.0/docs/Control-Exception.html#v:toException) must drop the context was added. Maybe it was added to make the behaviour of existing exception-rethrowing code outside of base more uniform?

To recap. In modern base, we have that specialized [`rethrowIO`](https://hackage-content.haskell.org/package/base-4.22.0.0/docs/Control-Exception.html#v:rethrowIO) function that does the right thing™ to propagate the context when rethrowing. All rethrowing code in base will use it. However, _outside_ of base, we’ll have lots of code like

```haskell
catchAndThrow :: IO a -> IO a
catchAndThrow = handle $ \(e :: SomeException) -> throwIO e

catchAndThrowIOException :: IO a -> IO a
catchAndThrowIOException = handle $ \(e :: IOException) -> throwIO e

```

that performs rethrows using the old [`throwIO`](https://hackage-content.haskell.org/package/base-4.22.0.0/docs/Control-Exception.html#v:throwIO). Rethrows of [`SomeException`](https://hackage-content.haskell.org/package/base-4.22.0.0/docs/Control-Exception.html#t:SomeException), but also rethrows of some concrete exception like `IOException`.

If we don’t require `toException` to drop the exception context:

- The exception that comes out of `catchAndThrow` will have two backtraces, the one from the original `SomeException` and the one added by the `throwIO`. Is that bad? Perhaps we could tolerate it and, by convention, assume that the last backtrace in the exception context is the original one. But perhaps it would confuse tooling like the new debugger. 🤔 As code begins to migrate to `rethrowIO`, double backtraces would become rarer.

- The exception that comes out of `catchAndThrowIOException` will have only one backtrace, the one added by the `throwIO`, because concrete exceptions don’t carry a context. We would need to switch to `rethrowIO` to keep the original backtrace. Is that lack of uniformity with respect to `SomeException` bad?

Alternatively, if we require `toException` to drop the exception context, in both `catchAndThrow` and `catchAndThrowIOException` the outgoing exception will only have the context added by the `throwIO`. Again, we would need to switch to `rethrowIO` to keep the original backtrace instead. And I would prefer `toException` to drop the context _even for [`ExceptionWithContext`](https://hackage-content.haskell.org/package/base-4.22.0.0/docs/Control-Exception.html#t:ExceptionWithContext)_, and have that separate `toExceptionPreservingContext` function mentioned earlier.

- `SomeException` and `ExceptionWithContext` rethrows with `throwIO` won’t include the original context. That would admittedly be a bit unintuitive.
- But it’s more uniform with re-throwing other types of exceptions using `throwIO`. A single “if you want the original context, switch to `rethrowIO`!” slogan applicable in all cases, instead of “well, you can switch to `rethrowIO`, but in the particular case of `SomeException` and `ExceptionWithContext` you might not need to do so, it depends…”
- Duplicate backtraces won’t ever happen.

---

<div class="post-metadata">

### Author: ![edsko](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/edsko/32/3024_2.png) [@edsko](https://discourse.haskell.org/u/edsko)
#### Post date: [May 20, 2026, 6:53am UTC](https://discourse.haskell.org/t/exception-annotations-lay-of-the-land/14056/6 "2026-05-20T06:53:09Z")

</div>

Personally I think the fact that `toException` clears the exception context is Very Bad ™. Yes, _if_ you happen to rethrow the exception inside a `catch`, then the `WhileHandling` saves you. And I agree, unnecessary `WhileHandling` annotations are annoying but ultimately harmless, and over time they will become less (indeed, I just submitted a [patch against `async`](https://github.com/simonmar/async/pull/185) which removes one example).

But–critically–exceptions are not always rethrown in the context of an exception handler; for example, consider [`waitSTM`](https://hackage-content.haskell.org/package/async-2.2.6/docs/src/Control.Concurrent.Async.Internal.html#waitSTM); this gets a previously thrown exception out of a `TMVar`, and rethrows it; since it uses `throwSTM`, and since it’s `SomeException`, all annotations on that exception are now irrevocably lost.

---

<div class="post-metadata">

### Author: ![parsonsmatt](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/parsonsmatt/32/949_2.png) [@parsonsmatt](https://discourse.haskell.org/u/parsonsmatt)
#### Post date: [May 22, 2026, 7:31pm UTC](https://discourse.haskell.org/t/exception-annotations-lay-of-the-land/14056/7 "2026-05-22T19:31:47Z")

</div>

> [@edsko](#):
>
> Personally I think the fact that `toException` clears the exception context is Very Bad ™.

I completely agree. I remember advocating heavily for the adoption of being part of a lot of the design discussions around this feature ([Original blog post](https://www.parsonsmatt.org/2022/08/16/dynamic_exception_reporting_in_haskell.html), [the library as it is today)](https://hackage.haskell.org/package/annotated-exception), and I do not at all recall this being a thing anyone needed or wanted.

[`annotated-exception`](https://hackage.haskell.org/package/annotated-exception)works today and on older GHCs and does not contain these warts nearly as much; but it is unfortunately limited for library development because it forces all consumers of your library to adopt `annotated-exception` and deal with the `AnnotatedException` wrapper. My next plan for the library is to figure out how to incorporate the new `ExceptionContext` changes without ruining the utility and UX of the library.

---

<div class="post-metadata">

### Author: ![danidiaz](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/danidiaz/32/92_2.png) [@danidiaz](https://discourse.haskell.org/u/danidiaz)
#### Post date: [May 23, 2026, 9:12pm UTC](https://discourse.haskell.org/t/exception-annotations-lay-of-the-land/14056/8 "2026-05-23T21:12:15Z")

</div>

Embarrassingly, I hadn’t considered `WhileHandling` then thinking about the rethrow behaviours. I was thinking about the possibility of ending up with multiple `Backtraces` annotations in the same annotation list.

I’ve created the [this repo](https://github.com/danidiaz/exception-rethrowing-experiments) to experiment (using GHC 9.14.1)

It has three cases:

```haskell
catchAndThrow :: IO a -> IO a
catchAndThrow action = catch @SomeException action throwIO

-- like 'catchAndThrow', but with a type of exception for which 'toException' 
-- preserves context, here we used 'ExceptionWithContext'
catchAndThrow' :: IO a -> IO a
catchAndThrow' action = catch @(ExceptionWithContext SomeException) action throwIO

-- after migrating to 'catchNoPropagate' + 'rethrowIO'
catchAndRethrow :: IO a -> IO a
catchAndRethrow action = catchNoPropagate @(ExceptionWithContext SomeException) action rethrowIO

```

In the first case, the exception structure looks like

```haskell
IOException
|
+- WhileHandling
| |
| `- IOException
| |
| `- Backtrace: HasCallStack backtrace:/ throwIO, called at app/Main.hs:30:7 ...
|
`- Backtrace: HasCallStack backtrace:/ throwIO, called at app/Main.hs:15:62 ...

```

The original exception is inside the `WhileHandling` annotation, with the original backtrace. The rethrown exception has the backtrace of the rethrow.

For the second case (the exception whose `toException` doesn’t clean the context) we have

```haskell
IOException
|
+- WhileHandling
| |
| `- IOException
| |
| `- Backtrace: HasCallStack backtrace:/ throwIO, called at app/Main.hs:30:7 ...
|
+- Backtrace: HasCallStack backtrace:/ throwIO, called at app/Main.hs:20:84 ...
|
`- Backtrace: HasCallStack backtrace:/ throwIO, called at app/Main.hs:30:7 ...

```

The `WhileHandling` is still there, but the rethrown exception (confusingly?) has two `Backtraces` annotations.

In the third case, the rethrow is not visible in the exception, we only have the original `Backtraces`:

```haskell
IOException
|
`- Backtrace: HasCallStack backtrace:/ throwIO, called at app/Main.hs:30:7 ...

```
