应捕获 AggregateException 后调用 Flatten() 解包,区分 OperationCanceledException(检查 CancellationToken 是否匹配)与其他异常,对非取消错误重抛,避免无差别吞异常。
直接 catch (AggregateException ex) 是最常见也最危险的做法——它会吞掉所有子异常,且无法区分哪些任务失败、哪些成功。真正该做的是解包并分类处理。
ex.Flatten(),否则嵌套层级可能很深(比如 Task.Run(() => Task.Run(...)) 套多层)InnerExceptions,逐个检查类型和消息,而不是只看 ex.InnerException
OperationCanceledException 的异常(如 NullReferenceException),说明不是取消导致的失败,需单独响应Task.WhenAll 中任一任务被取消,整个聚合异常里大概率包含 OperationCanceledException,但它不总是代表“用户主动取消”——也可能是超时或 CancellationTokenSource.Cancel() 被调用。关键看来源。
ex.InnerExceptions.OfType().Any(e => e.CancellationToken == yourToken) 确认是否由你的 token 触发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);
}
ConfigureAwait(false) 和 AggregateException 处理无关——它只影响 await 后的上下文捕获,不改变异常结构或类型。很多人加它是为了避免 UI 线程死锁,但误以为它能“简化异常”。事实是:
Task.WhenAll 本身不捕获异常,只是把所有子任务的异常打包进 AggregateException
ConfigureAwait,只要任务出错,await 表达式仍会抛出 AggregateException
await 后加 try/catch,以及是否调用 Flatten()
AggregateException 可能同时含取消、网络超时、空引用三类异常,而业务逻辑往往只关心其中一种。别图省事只 catch 顶层类型,拆开看才是常态。