appdomain.currentdomain.unhandledexception仅捕获未处理的同步异常,不处理ui线程、task异步异常及托管宿主拦截的异常,且无法阻止程序退出。

AppDomain.CurrentDomain.UnhandledException 不是全局异常兜底,它只收“漏网”的同步异常,且无法阻止程序退出。 你注册了却没日志、点了按钮照样弹系统报错框、Task里抛异常完全没反应——这些都不是配置错了,而是它本来就不该管那些事。
AppDomain.UnhandledException 只能捕获哪些异常
它只在主线程或未被 try/catch 包裹的普通线程(Thread)中,抛出后**彻底没人接住**的异常时触发。比如:
-
Main()函数末尾直接throw new NullReferenceException() - 用
new Thread(() => { throw new Exception(); }).Start()启动的线程里没写try/catch
但以下情况它完全收不到:
- WinForms 按钮点击里崩了 → 走
Application.ThreadException - WPF 绑定表达式求值失败 → 走
Application.DispatcherUnhandledException -
Task.Run(() => { throw new Exception(); })→ 异常封在Task对象里,除非你.Wait()或await,否则不会冒泡到这里 - ASP.NET Core 中间件里抛异常 → 已被框架提前捕获并处理,根本到不了 AppDomain 层
-
StackOverflowException、OutOfMemoryException→ .NET Core/.NET 5+ 默认不触发该事件,强行监听可能干扰运行时
为什么注册了却没触发
常见原因不是代码写错,而是时机或宿主机制导致它压根没机会执行:
- 注册位置太晚:必须在
Main()最开头,比如放在Application.Run(new MainForm())之后,UI 崩溃时事件早已错过 - .NET 6+ 控制台或 Web 宿主默认静默异常:
IHostBuilder启动阶段的配置错误(如appsettings.json格式错)会在任何事件注册前就崩溃,外层得用try/catch包住host.RunAsync() - 调试器干扰:有些异常在 VS 调试下会先被调试器拦截,发布后反而不触发,造成“本地有日志、线上静默退出”的假象
- 日志组件自己崩了:在事件处理器里调用
NLog.LogManager.GetCurrentClassLogger().Error(...),结果磁盘满或配置路径错,导致二次异常被吞,原异常线索全丢
WinForms/WPF/Task 必须补的三块拼图
要真正覆盖常见崩溃路径,AppDomain.CurrentDomain.UnhandledException 只是其中一块,缺一不可:
- WinForms UI 线程:必须调用
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException),再订阅Application.ThreadException,并在 handler 里设e.Handled = true抑制系统弹窗(注意:这不能让程序继续安全运行,通常跟Application.Exit()配合) - WPF UI 线程:在
App.xaml.cs的OnStartup中订阅this.DispatcherUnhandledException,同样需e.Handled = true;async void方法里的异常仍不会进来,得靠TaskScheduler.UnobservedTaskException补漏 - 异步任务(
Task):订阅TaskScheduler.UnobservedTaskException,并在 handler 中调用e.SetObserved()防止 GC 时再触发;注意它只对“从未被观察”的异常有效,await或.Result就算已观察
最易被忽略的是:所有这些事件处理器里,都不能做耗时操作(比如网络请求、复杂文件写入),也不能再抛异常。e.IsTerminating 为 true 时,运行时已进入终止流程,状态不可信——日志写入失败、UI 弹窗卡死、甚至 Console.WriteLine 都可能静默失败。稳住的关键,是把日志逻辑压到最简,优先保障路径可写、格式最小化、避免依赖外部服务。











