应捕获 AggregateException 后调用 Flatten() 解包,区分 OperationCanceledException(检查 CancellationToken 是否匹配)与其他异常,对非取消错误重抛,避免无差别吞异常。

Task.WhenAll 抛出的 AggregateException 怎么捕获
直接 catch (AggregateException ex) 是最常见也最危险的做法——它会吞掉所有子异常,且无法区分哪些任务失败、哪些成功。真正该做的是解包并分类处理。
- 必须调用
ex.Flatten(),否则嵌套层级可能很深(比如Task.Run(() => Task.Run(...))套多层) - 解包后遍历
InnerExceptions,逐个检查类型和消息,而不是只看ex.InnerException - 如果其中混有非
OperationCanceledException的异常(如NullReferenceException),说明不是取消导致的失败,需单独响应
如何区分取消异常和真实错误
Task.WhenAll 中任一任务被取消,整个聚合异常里大概率包含 OperationCanceledException,但它不总是代表“用户主动取消”——也可能是超时或 CancellationTokenSource.Cancel() 被调用。关键看来源。
- 用
ex.InnerExceptions.OfType确认是否由你的 token 触发().Any(e => e.CancellationToken == yourToken) - 若存在其他异常(如
HttpRequestException、SqlException),应优先记录并告警,不能和取消混为一谈 - 不要依赖
ex.Handle(_ => true)无差别吞掉所有异常,这会让调试变得极其困难
实际捕获与重抛建议写法
下面这段代码展示了安全解包、分类、并选择性重抛的典型模式:
try
{
await Task.WhenAll(tasks);
}
catch (AggregateException ae)
{
var flattened = ae.Flatten();
var cancellationErrors = flattened.InnerExceptions.OfType()
.Where(e => e.CancellationToken == cancellationToken).ToList();
var otherErrors = flattened.InnerExceptions.Except(cancellationErrors).ToList();
if (otherErrors.Count > 0)
{
// 至少有一个非取消错误:构造新 AggregateException 并重抛
throw new AggregateException("One or more errors occurred during parallel execution.", otherErrors);
}
// 全是预期中的取消:按业务逻辑静默处理或返回默认值
return default(TResult);
}
为什么不用 await Task.WhenAll(...).ConfigureAwait(false)
ConfigureAwait(false) 和 AggregateException 处理无关——它只影响 await 后的上下文捕获,不改变异常结构或类型。很多人加它是为了避免 UI 线程死锁,但误以为它能“简化异常”。事实是:
-
Task.WhenAll本身不捕获异常,只是把所有子任务的异常打包进AggregateException - 无论是否配置
ConfigureAwait,只要任务出错,await 表达式仍会抛出AggregateException - 真正影响异常行为的是你有没有在
await后加try/catch,以及是否调用Flatten()
AggregateException 可能同时含取消、网络超时、空引用三类异常,而业务逻辑往往只关心其中一种。别图省事只 catch 顶层类型,拆开看才是常态。










