C产品如何满足特定用户需求?
- 内容介绍
- 文章标签
- 相关推荐
本文共计1014个文字,预计阅读时间需要5分钟。
直接捕获`AggregateException`而不展开,等同于没有处理——它只是一个异常容器,真正的问题隐藏在`InnerExceptions`中。
为什么不能只 catch (AggregateException ex) 就完事
因为 AggregateException 本身不表示具体错误类型,它只是多个异常的“打包盒”。你看到的 ex.Message 通常是 “One or more errors occurred.” 这种无意义提示;ex.InnerException 只返回第一个子异常,其余全被忽略。更危险的是:如果里面混着 OperationCanceledException 和 SqlException,不展开就无法区分是用户点了取消按钮,还是数据库连不上。
必须调用 ex.Flatten(),否则嵌套可能很深(比如 Task.Run(() => Task.Run(() => throw new Exception())) 会生成两层 AggregateException)。
以下操作容易踩坑:
- 仅遍历
ex.InnerExceptions却没调用Flatten()→ 漏掉深层异常 - 用
ex.Handle(_ => true)吞掉所有异常 → 日志空白、故障不可追溯 - 只检查
ex.InnerException→ 永远只能看到第一个失败任务
如何正确展开并分类处理 InnerExceptions
核心动作是:先 Flatten(),再逐个 inspect 类型和上下文。不是所有异常都该重抛,也不是所有取消都该静默忽略。
典型处理逻辑如下:
- 用
ex.Flatten().InnerExceptions获取扁平化后的全部原始异常 - 对每个
inner判断:inner is OperationCanceledException canceled && canceled.CancellationToken == yourCts.Token→ 确认是否由你控制的取消触发 - 遇到非取消类异常(如
HttpRequestException、NullReferenceException)应立即记录日志并告警,不能和取消混为一谈 - 若同时存在取消 + 真实错误,优先响应真实错误;取消可作为补充上下文记录
示例片段:
try { await Task.WhenAll(tasks); } catch (AggregateException ae) { var flattened = ae.Flatten(); foreach (var inner in flattened.InnerExceptions) { if (inner is OperationCanceledException ce && ce.CancellationToken == cts.Token) { // 是我们发起的取消,可忽略或记录为“已取消” continue; } // 其他异常:记录、上报、或按业务逻辑重抛 LogError(inner); throw; // 或 throw new InvalidOperationException("批量操作部分失败", inner); } }
WaitAll / Result / await 三种触发点的异常行为差异
这三者都会导致 AggregateException 抛出,但时机和语义不同:
-
Task.WaitAll(tasks):同步阻塞,异常在调用点立刻抛出;适合简单批处理场景 -
task.Result:同步取值,同样立刻抛出;但若 task 已完成且成功,不会额外开销;注意 UI 线程调用会卡死 -
await Task.WhenAll(tasks):异步等待,异常仍包装为AggregateException,但堆栈更清晰,推荐用于现代 async/await 流程
关键共性:三者都不自动展开异常。无论用哪个,只要没调用 Flatten(),你就只拿到了一个“盒子”,没打开看里面。
别忘了未观察异常的兜底机制
如果某个 Task 抛了异常,但你既没 await 它、也没访问 Exception 属性、也没用 Wait(),.NET 会在 GC 时把异常提升为 UnobservedTaskException —— 在 .NET 6+ 默认终止进程。
生产环境务必注册兜底监听:
TaskScheduler.UnobservedTaskException += (sender, e) => { LogCritical("Unobserved exception", e.Exception); e.SetObserved(); // 标记为已观察,避免进程退出 };
但这只是最后一道防线,不能替代主动展开和分类处理。真正的难点从来不在“怎么捕获”,而在于“怎么判断哪个异常该忽略、哪个该报警、哪个该重试”。
本文共计1014个文字,预计阅读时间需要5分钟。
直接捕获`AggregateException`而不展开,等同于没有处理——它只是一个异常容器,真正的问题隐藏在`InnerExceptions`中。
为什么不能只 catch (AggregateException ex) 就完事
因为 AggregateException 本身不表示具体错误类型,它只是多个异常的“打包盒”。你看到的 ex.Message 通常是 “One or more errors occurred.” 这种无意义提示;ex.InnerException 只返回第一个子异常,其余全被忽略。更危险的是:如果里面混着 OperationCanceledException 和 SqlException,不展开就无法区分是用户点了取消按钮,还是数据库连不上。
必须调用 ex.Flatten(),否则嵌套可能很深(比如 Task.Run(() => Task.Run(() => throw new Exception())) 会生成两层 AggregateException)。
以下操作容易踩坑:
- 仅遍历
ex.InnerExceptions却没调用Flatten()→ 漏掉深层异常 - 用
ex.Handle(_ => true)吞掉所有异常 → 日志空白、故障不可追溯 - 只检查
ex.InnerException→ 永远只能看到第一个失败任务
如何正确展开并分类处理 InnerExceptions
核心动作是:先 Flatten(),再逐个 inspect 类型和上下文。不是所有异常都该重抛,也不是所有取消都该静默忽略。
典型处理逻辑如下:
- 用
ex.Flatten().InnerExceptions获取扁平化后的全部原始异常 - 对每个
inner判断:inner is OperationCanceledException canceled && canceled.CancellationToken == yourCts.Token→ 确认是否由你控制的取消触发 - 遇到非取消类异常(如
HttpRequestException、NullReferenceException)应立即记录日志并告警,不能和取消混为一谈 - 若同时存在取消 + 真实错误,优先响应真实错误;取消可作为补充上下文记录
示例片段:
try { await Task.WhenAll(tasks); } catch (AggregateException ae) { var flattened = ae.Flatten(); foreach (var inner in flattened.InnerExceptions) { if (inner is OperationCanceledException ce && ce.CancellationToken == cts.Token) { // 是我们发起的取消,可忽略或记录为“已取消” continue; } // 其他异常:记录、上报、或按业务逻辑重抛 LogError(inner); throw; // 或 throw new InvalidOperationException("批量操作部分失败", inner); } }
WaitAll / Result / await 三种触发点的异常行为差异
这三者都会导致 AggregateException 抛出,但时机和语义不同:
-
Task.WaitAll(tasks):同步阻塞,异常在调用点立刻抛出;适合简单批处理场景 -
task.Result:同步取值,同样立刻抛出;但若 task 已完成且成功,不会额外开销;注意 UI 线程调用会卡死 -
await Task.WhenAll(tasks):异步等待,异常仍包装为AggregateException,但堆栈更清晰,推荐用于现代 async/await 流程
关键共性:三者都不自动展开异常。无论用哪个,只要没调用 Flatten(),你就只拿到了一个“盒子”,没打开看里面。
别忘了未观察异常的兜底机制
如果某个 Task 抛了异常,但你既没 await 它、也没访问 Exception 属性、也没用 Wait(),.NET 会在 GC 时把异常提升为 UnobservedTaskException —— 在 .NET 6+ 默认终止进程。
生产环境务必注册兜底监听:
TaskScheduler.UnobservedTaskException += (sender, e) => { LogCritical("Unobserved exception", e.Exception); e.SetObserved(); // 标记为已观察,避免进程退出 };
但这只是最后一道防线,不能替代主动展开和分类处理。真正的难点从来不在“怎么捕获”,而在于“怎么判断哪个异常该忽略、哪个该报警、哪个该重试”。

