AppDomain.UnhandledException仅捕获主线程和未捕获的后台线程异常,无法处理UI线程、Task或ASP.NET Core中间件异常;必须在Main()开头注册,且不能阻止程序退出。

AppDomain.UnhandledException 只能捕获主线程和后台线程的“漏网之鱼”
AppDomain.CurrentDomain.UnhandledException 是最常被误用的全局异常入口。它**不处理 UI 线程异常、不处理 Task 异常、不处理 ASP.NET Core 中间件层已吞掉的异常**,只在主线程或未被 try/catch 包裹的后台线程中,抛出又没人接住的异常(比如 NullReferenceException)时触发。
它有两个硬限制:无法阻止程序退出,必须在 Main() 最开头注册——晚一秒,就可能错过第一个异常。
- 注册位置错误:放在
Application.Run(...)之后,UI 崩溃时根本收不到 - 误以为能拦住 WinForms 按钮点击里的空引用:实际走的是
Application.ThreadException,不是这里 - 在 .NET 6+ 控制台或 ASP.NET Core 中,默认宿主会提前捕获并静默异常,导致这个事件压根不触发
WinForms 必须配 Application.ThreadException,否则 UI 崩溃弹窗拦不住
WinForms 的 UI 线程异常(比如按钮点击里访问了已释放的控件)由 Windows 消息循环直接拦截,AppDomain.UnhandledException 完全收不到。必须显式注册 Application.ThreadException,并在处理函数中设 e.Handled = true 才能阻止系统默认弹窗。
注意:e.Handled = true 只是不让 Windows 弹框,并不能让程序继续运行——UI 线程通常已处于不可恢复状态,后续操作大概率再崩。所以常见做法是记录日志 + 友好提示 + Application.Exit()。
- 没调用
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException):部分异常仍会绕过ThreadException直接弹窗 - 处理函数里忘了
e.Handled = true:用户看到两个弹窗(你的 MessageBox + 系统报错框) - 在非 STAThread 线程里注册:事件根本不会被触发
WPF 要用 DispatcherUnhandledException,别拿 ThreadException 兼容
WPF 的 UI 线程叫 Dispatcher 线程,和 WinForms 的消息循环机制不同。Application.ThreadException 对 WPF 完全无效。必须监听 Application.DispatcherUnhandledException,且同样需要手动设 e.Handled = true 来抑制默认行为。
这个事件只捕获 Dispatcher 上未处理的异常,比如绑定表达式求值失败、命令执行抛异常、或者后台线程用 Dispatcher.Invoke 时抛出的异常。但注意:async void 方法里抛的异常也不会进这里,得靠 TaskScheduler.UnobservedTaskException 补漏。
- 在
App.xaml.cs的OnStartup里注册,而不是Main():WPF 启动流程不同,放错位置会漏注册 - 设了
e.Handled = true却没做任何日志记录:异常彻底消失,排查无从下手 - 混淆
DispatcherUnhandledException和AppDomain.UnhandledException的职责:前者管 UI 线程,后者管非 UI 线程,两者不重叠也不能替代
Task 异常必须用 UnobservedTaskException,否则 GC 会悄悄终止进程
未 await 的 Task 抛异常,或 await 后没检查 Exception 属性,异常就会变成“未观察到的”。.NET 运行时会在 GC 时触发 TaskScheduler.UnobservedTaskException,若不处理,**下次 GC 可能直接终止进程**(尤其在 .NET Framework 中;.NET Core/5+ 默认不终止,但仍会输出警告日志)。
正确做法是在事件处理器中调用 e.SetObserved(),告诉运行时“我看见了”,避免后续干扰。但要注意:这仅防止进程终止,不代表异常可忽略——它往往暴露了资源泄漏、状态不一致等深层问题。
- 只注册了
AppDomain.UnhandledException就以为 Task 异常也覆盖了:完全不生效 - 在
UnobservedTaskException里只写日志却不调用e.SetObserved():.NET Framework 下进程可能随机崩溃 - 把异步逻辑全塞进
async void:异常既不进DispatcherUnhandledException,也不进UnobservedTaskException,真正意义上的“黑洞”
全局异常捕获不是注册一个事件就万事大吉的事。每个线程模型、每种异步形态都有自己的异常逃逸路径,缺一环,崩溃日志就断一截。最容易被忽略的是:你写的日志代码本身,也可能在异常上下文中抛新异常——比如写文件时磁盘满、序列化时对象循环引用。确保日志组件足够轻量、有 fallback 输出(如 Trace.WriteLine),才是真正在生产环境“看得见崩溃”的底线。










