子线程throw的异常主线程无法捕获,.NET不会自动冒泡;应优先用Task/async-await,裸Thread需手动try-catch并传递异常;Task异常封装为AggregateException,await可自动解包,task.Wait()/Result则抛AggregateException;async void异常无法捕获且会崩溃进程,仅限UI事件且须内部try-catch;UnobservedTaskException非兜底机制,仅记录被忽略异常,不可依赖。

子线程里 throw 的异常根本不会被主线程 try-catch 捕获
这是最常被误解的一点:直接在 Thread 启动的方法里 throw,主线程的 try/catch 完全收不到。.NET 不会把子线程异常“冒泡”到创建它的线程,而是直接终止该线程,并可能让整个进程崩溃(尤其在 .NET Framework 旧版本中)。
实操建议:
- 别用裸
Thread做需要错误反馈的任务;改用Task或async/await - 如果必须用
Thread,得自己加包装:在线程入口方法里try/catch,把异常存到共享变量或通过回调通知主线程 -
Thread的UnobservedTaskException事件不适用于裸线程,只对Task生效
Task.Run 中 throw 会触发 AggregateException,不是原始异常类型
Task 把多个可能的异常打包进 AggregateException,哪怕只抛了一个 NullReferenceException,你 catch 到的也是它,不是原异常本身。
常见错误现象:写 catch (NullReferenceException) 却捕不到,因为实际抛出的是 AggregateException。
实操建议:
- 总是先
catch (AggregateException ex),再用ex.InnerException或ex.Flatten().InnerExceptions取出原始异常 - 更推荐用
await:异步方法里throw会被自动解包,await task后catch到的就是原始异常类型 -
task.Wait()和task.Result都会抛AggregateException;await task不会
async void 方法里的异常会直接炸掉进程,无法捕获
async void 是唯一能让异常“逃逸”出所有 try/catch 的合法 C# 写法。它没有返回 Task,所以没人能观察它、等待它、处理它的异常。
使用场景:仅限事件处理器(比如按钮点击),且必须确保内部 try/catch 住所有可能异常。
实操建议:
- 永远不要写
async void DoWork()这种独立方法;改成async Task DoWork() - UI 事件中若要用 async,必须写成
async void Button_Click(...),但里面第一行就要try { ... },否则 UI 线程崩溃 -
AppDomain.UnhandledException和TaskScheduler.UnobservedTaskException都救不了async void的异常
TaskScheduler.UnobservedTaskException 只救“没被 await 也没被 Wait/Result”的异常
这个事件不是万能兜底。它只在 Task 异常被彻底“忽略”——既没 await、也没 .Wait()、也没读 .Exception 属性——且该 Task 被 GC 回收时才触发。
性能 / 兼容性影响:.NET Core 2.0+ 默认已禁用此事件(ThreadPoolTaskScheduler.UnobservedTaskException 不再抛出),只记录日志;老版 .NET Framework 会真的终止进程。
实操建议:
- 别依赖它做错误处理,它只是最后的“尸体发现器”,不是“急救员”
- 真正可靠的做法是:每个
Task都要被显式消费——要么await,要么.Wait()+catch (AggregateException),要么主动访问.Exception - 在调试阶段可临时开启:
TaskScheduler.UnobservedTaskException += (s, e) => { Debugger.Break(); };
最容易被忽略的是:即使用了 await,如果上层调用者没 await 你的 async Task 方法(比如忘了写 await 或当成同步调用),异常照样会变成未观测状态。异常传播链条是否完整,取决于每一层是否真正参与了 Task 的生命周期管理。


















