There are two kinds of threads executing inside managed code: the ones we start in managed code and the ones that wander into the CLR. The ones that started in managed code include all calls to Thread.Start(), the managed threadpool threads, and the Finalizer thread(s).
For all the threads we start in managed code, the CLR has its own exception backstop and nothing will leak out to the OS unhandled exception filter. We can call the AppDomain.UnhandledException event from this backstop.
For the ones that wander into managed code, we are registered on the OS unhandled exception filter so we can call the AppDomain.UnhandledException from there. Of course, different unmanaged components are registered in a rather random order on the OS unhandled exception filter. Some of them chain nicely. Others (like the VC6 filter) might decide to rip the process without chaining under certain circumstances.
So you are only completely assured of getting the AppDomain’s UnhandledException event on the managed threads.
Another subtlety is that you must register for this event in the Default AppDomain. That’s because – by the time the exception is unhandled – the thread has unwound out to the default AppDomain. That’s where all threads start executing and that’s where they eventually unwind to. This is an unfortunate restriction for some scenarios, but it’s not clear whether it will ever be relaxed.
Finally, the CLR has some default policy for which unhandled exceptions should terminate the process and which ones should be swallowed. Generally, unhandled exceptions on threadpool threads, the Finalizer thread(s) and similar “reusable” threads are swallowed. We simply terminate the current unit of work and proceed to the next one. Unhandled exceptions on the main thread of a managed executable will terminate the process.
Nobody particularly likes the defaults that were chosen. But everyone seems to have conflicting opinions on what would have made better defaults. And, at this point, nothing is likely to change. The UnhandledException event is there so that you can install your own policy. You can terminate the process, log the failure, trap to the debugger, swallow the exception or any other behavior.