Task.Run中抛异常若未await或Wait,主线程无法捕获,异常静默丢失;必须await、Wait或GetResult才能传播;async void异常直接炸给SynchronizationContext;await后异常自动展开为原始类型,无需手动处理AggregateException。
Task.Run里抛异常,主线程根本收不到
直接在 task.run 里 throw 异常,又没 await,那异常会进 task.exception,但不会崩主线程,也不会被 try-catch 捕获。这是最常踩的坑:以为“写了 try 就稳了”,结果异常静默丢失。
- 必须
await这个Task,或者调用Wait()/GetAwaiter().GetResult(),才能让异常冒泡到当前上下文 - 如果只是 fire-and-forget(比如日志上报),得手动检查
task.Exception,否则就真丢了 -
async void方法里抛异常?直接炸给 SynchronizationContext,极难捕获,永远别这么写
await 一个失败的 Task,异常类型会变包装
原异常是 ArgumentNullException,但 await 后抛出来的是 AggregateException,里面套着一层 —— 这是因为 .NET 把 Task 内部所有异常都统一塞进 AggregateException.InnerExceptions。不拆包直接 catch AggregateException,容易漏判。
- 推荐写法:
catch (ArgumentNullException ex)——await会自动展开最内层异常,让你直接 catch 原始类型 - 只有在用
Task.WaitAll或Task.WhenAll且多个任务失败时,才真正需要处理AggregateException - 如果用了
ContinueWith,默认不传播异常,得显式写TaskContinuationOptions.OnlyOnFaulted并读antecedent.Exception
async/await 方法里 try-catch 放错位置,照样漏异常
很多人把 try-catch 包在 async 方法体外层,比如:try { DoWorkAsync(); } catch {...}。这等于只 catch 了 Task 创建阶段的异常(比如构造参数错),根本 catch 不到异步执行中抛的异常。
- 正确做法:try-catch 必须包在
await表达式周围,或整个 async 方法内部 - 如果方法返回
Task而非Task<t></t>,且你不想 await,那就只能用.ContinueWith(t => { if (t.IsFaulted) ... })手动捞异常 - 注意
using和await的顺序:在 async 方法里,await前的资源如果已 dispose,后续 await 可能触发 ObjectDisposedException
ConfigureAwait(false) 不影响异常传播路径
有人以为加了 ConfigureAwait(false) 就会让异常“不回 UI 线程”所以更安全,其实它只影响 await 后续代码的执行上下文,对异常怎么抛、在哪抛完全没影响。异常该在哪崩还在哪崩。
-
ConfigureAwait(false)的唯一作用是避免死锁(比如 WinForms/WPF 中 await 后强行切回 UI 线程),和异常捕获无关 - 如果你在 ASP.NET Core(无同步上下文)里用,加不加都一样;但在 WinForms 里不加,又在 UI 线程 await 一个未完成的 Task,就可能死锁
- 异常对象本身不会因为
ConfigureAwait被修改或吞掉,放心捕获原始类型就行
异步异常真正的复杂点不在语法,而在执行流分裂后,错误发生的位置和被捕获的位置可能隔了几个栈帧、甚至跨线程。只要记住:没 await 就没传播,没 await 就没原始异常类型,其他都是枝节。











