External Publication
Visit Post

Exception Annotations: Lay of the Land

Haskell Community [Unofficial] May 16, 2026
Source

I don’t know why the invariant that 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 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

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. Rethrows of 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 , 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.

Discussion in the ATmosphere

Loading comments...