Haskell libraries are written this way all the time. Exception-safe libraries with async-aware handling are pretty commonplace nowadays (partially thanks to safe-exceptions and unsafeio providing a nice API to deal with them.)
- the shared-nothing structuring means it’s harder to corrupt external - that processes can die at any moment is part of the culture - the supervision tree means the most likely case for killing an other process is a parent killing a misbehaving child, which they can then handle as if the child had faulted
Some of it too is just Erlang being too, err, for lack of a better word "weak" to have some of the problematic resources. Since Erlang has no "locks", you simply can't take a lock and then kill the thread responsible for unlocking it. You can construct such locks at a higher level, but Erlang's design tends to encourage other designs. For instance, if I were designing a higher-level lock, I'd design it to link to the process taking the lock and release it if it dies. This can still theoretically get you into trouble but especially if this is documented as the semantics of the lock, you have to work a lot harder at it.
If you really worked at it you could manage to screw up your Erlang system with thread killing through your own lock implementation and such, but it still will be something you can recover from the REPL, you won't confuse BEAM itself. In an imperative language you'd be ranging from lucky to be able to reach in somehow and fix it to there simply being no way.
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.
That said, the other big difference with Haskell is the low-level API actually provides functionality to solve the exact problem of an async exception being raised anywhere via `mask`. It's still hard to use correctly but at least it's possible.
It also has good learning material on the subject in the form of thorough blog posts.
It has robust, types abstractions for both throwing, catching, and masking exceptions.
Its exception runtime implementation is better built too.
[1]: https://hackage.haskell.org/package/base-4.20.0.1/docs/Contr...
[2]: https://hackage.haskell.org/package/base-4.20.0.1/docs/Contr...
Pretty funny that Haskell of all languages also has best-in-class exceptions & exception handling haha.