There are also plenty of footguns when you attempt to catch exceptions. A piece of code might intend to catch all synchronous exceptions (raised in the current thread) but accidentally catches asynchronous exceptions. Again this is a novice issue. When that happens, cancelling a thread doesn't work because thread cancellation is implemented with async exceptions.
What makes things good is that firstly pure code can only raise exceptions but not catch them. (If you need error handling in pure code, you use the Either monad not exceptions.) This dramatically reduces chances of coding mistakes because exceptions aren't as overused as some other languages. Secondly the Haskell community more or less shuns on direct use of these low-level exception handling APIs and everyone uses the async library https://hackage.haskell.org/package/async-2.2.5/docs/Control... which solves not only this problem but also provides nice abstractions to build concurrent computations. Check out its Concurrently type: you now have an Applicative that represents concurrent computations which reuse all your intuition from all Applicative instances. You don't even need to manually create threads or kill threads. Overall insofar as Haskell being the only language where throwing exceptions at another thread isn't a box of footguns, it's only because the language allows a sufficient amount of abstraction power that frees the programmer from using the error-prone low-level APIs. The actual low-level APIs are still full of footguns.