# Catching (async) exceptions

**URL:** <https://discourse.haskell.org/t/catching-async-exceptions/14941>\
**Category:** Learn\
**Created:** [November 3, 2026, 2:51pm UTC](https://discourse.haskell.org/t/catching-async-exceptions/14941 "2026-11-03T14:51:17Z")\
**Posts on this page:** 2\
**Page:** 1

<div class="post-metadata">

**Author:** ![magthe](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/magthe/32/1362_2.png) [@magthe](https://discourse.haskell.org/u/magthe)\
**Post date:** [November 3, 2026, 2:51pm UTC](https://discourse.haskell.org/t/catching-async-exceptions/14941/1 "2026-11-03T14:51:17Z")

</div>

In my attempts at improving how we use Haskell at work I’m trying to make sure k8s doesn’t silently kill our services when running out of memory. So I’ve added `+RTS -MxM`, where `xM` is a reasonable amount less than the available memory. That lead to some services throwing `HeapOverflow` exceptions. We have some rules about the format of our logs and the default exception handler (`defaultHandler`, defined in [uncaughtExceptionHandler](https://hackage-content.haskell.org/package/ghc-internal-9.1401.0/docs/src/GHC.Internal.Conc.Sync.html#uncaughtExceptionHandler)) doesn’t come close. So, I thought I’d play around a little to see what my options are, but I very quickly got confused. I started looking at [forkIO](https://hackage-content.haskell.org/package/base-4.21.1.0/docs/Control-Concurrent.html#v:forkIO).

It seems that the default exception handler (`defaultHandler`, defined in [uncaughtExceptionHandler](https://hackage-content.haskell.org/package/ghc-internal-9.1401.0/docs/src/GHC.Internal.Conc.Sync.html#uncaughtExceptionHandler)) doesn’t handle async exceptions at all. Even though the documentation of `forkIO` says it does. Well, it does say it “passes all other exceptions to the uncaught exception handler”, I assumed that’d be the one I can set using `setUncaughtExceptionHandler`, but setting it has no effect.

I’ve found that this code (mostly) works

```haskell
module Main where

import Control.Concurrent (forkFinally, forkIO, myThreadId, threadDelay)
import Control.Exception (SomeException, handle, throwIO)
import GHC.Conc (setUncaughtExceptionHandler)

main :: IO ()
main = do
    handle myExceptionHandler $ do
        putStrLn "13: trigger a sync exception in a forked thread - with my explicit exception handler"
        thrid <- forkIO threadFunc
        putStrLn $ "forked thread ID: " <> show thrid
        threadDelay 2_500_000
    putStrLn "time to exit"

threadFunc :: IO ()
threadFunc = do
    putStrLn "running thread"
    let f x = 1 + f x
    print $ f 1

myExceptionHandler :: SomeException -> IO ()
myExceptionHandler e = do
    myId <- myThreadId
    putStrLn $ str myId
  where
    str tid = "caught exception on " <> show tid <> ": " <> show e

```

I compiled it using `ghc --make -threaded -rtsopts -o exp13 exp13.hs`.

I write “mostly” because most times I get the expected output when running it

```haskell
> ./exp13 +RTS -M50M; print $?
13: trigger a sync exception in a forked thread - with my explicit exception handler
running thread
forked thread ID: ThreadId 5
caught exception on ThreadId 4: heap overflow
time to exit
0

```

But then, every now and then I get this

```haskell
./exp13 +RTS -M50M; print $?
13: trigger a sync exception in a forked thread - with my explicit exception handler
running thread
forked thread ID: ThreadId 5
caught exception on ThreadId 4: heap overflow
exp13: Heap exhausted;
exp13: Current maximum heap size is 52428800 bytes (50 MB).
exp13: Use `+RTS -M<size>' to increase it.
251

```

It looks like _both_ my exception handler and the default where triggered. Surely that should _never_ happen, right?

```haskell
ghc --version
The Glorious Glasgow Haskell Compilation System, version 9.12.3

```

---

<div class="post-metadata">

**Author:** ![magthe](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/magthe/32/1362_2.png) [@magthe](https://discourse.haskell.org/u/magthe)\
**Post date:** [November 3, 2026, 2:56pm UTC](https://discourse.haskell.org/t/catching-async-exceptions/14941/2 "2026-11-03T14:56:19Z")

</div>

I just realised that I ought to run with `-N` too, and now I can’t seem to reproduce the behaviour from above.

I did see some even stranger behaviour when I’d forgotten the `-threaded` flag, So is that the cause, I _must_ use `-threaded` when compiling and at least 2 capabilities, `-N2`, when running it to guarantee the expected behaviour?
