异常处理中,catch 与日志应该放在哪里
· 小村 豪 · 异常处理, 日志, 错误处理, 设计, C# / .NET
目录
- 先说结论
catch、日志、错误处理是三件不同的事- 2.1. 进行
catch - 2.2. 记录日志
- 2.3. 进行错误处理
- 2.4. 对异常进行翻译
- 2.1. 进行
- 先看这张判断表
- 在调用层级的哪个位置做什么
- 4.1. 最深层的 helper / utility / private method
- 4.2. 外部 I/O 边界:Repository / Gateway / SDK 封装
- 4.3. Application Service / UseCase
- 4.4. UI / HTTP / Job / Message 边界
- 4.5. 最后的未处理异常处理程序
- 4.6. 用一条调用链来看
- 区分预期内的失败与预期外的异常
- 日志应该在哪里、记录几次
- 常见的 NG 做法
- 审查时的检查清单
- 简单的使用区分
- 总结
- 参考资料
- 相关文章
1. 先说结论
- 原则是:不要在深层大范围
catch。catch的位置应靠近能够定义失败单位的 边界。 - 日志的基本原则是:针对一次失败,只记录一条主日志。如果每一层都对同一个异常持续记录
Error,会让阅读的人感到困惑。 - 最深层的职责是 收尾、局部回滚、异常翻译,必要时进行有限的 retry。如果要重新抛出,通常不在这里记录主日志。
- 界面操作、HTTP 请求、一个作业、一条消息处理这类 处理边界,最容易成为最自然的主日志记录点。
- 预期内的失败应该在对应用例(use case)的单位上进行 结果化处理。不必把所有失败都作为异常一路往上抛。
AppDomain.UnhandledException、WPF 的DispatcherUnhandledException、WinForms 的ThreadException、ASP.NET Core 的异常处理程序,以及宿主的最终异常处理,与其说是恢复点,更应该看作最后的记录点。- 因用户取消或关闭而产生的
OperationCanceledException,通常 不作为 Error 处理。 - 拿不准的时候,按以下顺序判断:
- 在这个位置真的能做出判断吗
- 在这里能明确失败的单位吗
- 在这里能恢复状态,或重新构建状态吗
- 如果在这里记录日志,上层是否会对同一个异常再记录一次
总而言之,基本原则是:不是”哪里能 catch 就在哪里 catch”,而是在能够负责判断的地方接收异常。
2. catch、日志、错误处理是三件不同的事
2.1. 进行 catch
catch 是先接收异常,再改变处理流程。但这本身并不等于 恢复。
例如,即使在下层方法中接收了异常,
- 不知道该向用户展示什么
- 不知道这个失败应该让整个界面停止,还是只让本次操作失败即可
- 不知道该 request 或 job 是否可以继续
如果是这种情况,这个位置往往并不适合 catch。
2.2. 记录日志
日志不仅是记录”发生了异常”这个事实,更是为了日后能够追溯 哪项工作失败了 的记录。
因此,好的日志记录点通常具备以下要素中的某一个或几个。
- requestId / traceId
- userId
- orderId / fileId / batchId
- 第几条输入
- 是哪次界面操作
- 是哪个队列、哪条消息
较深的 helper 或公共函数,即便掌握技术细节,往往也不具备这些上下文。 因此,掌握技术细节的位置 和 掌握运维上下文的位置,往往并不是同一个地方。
2.3. 进行错误处理
这里所说的错误处理,指的是以下这类操作。
- 在界面上显示错误消息
- 在 HTTP 中返回 4xx / 5xx
- 将这一条视为失败,继续处理下一条
- 重新初始化该 subsystem
- 终止进程,交由重启机制处理
- 释放资源并安全退出
也就是说,这是在 决定调用方或用户所看到的失败形态。
2.4. 对异常进行翻译
在实务中,catch 与”处理”之间,还有一项重要的工作,那就是 翻译。
例如,
HttpRequestExceptionIOExceptionJsonException- DB 驱动特有的异常
- vendor SDK 特有的异常
如果把这些原样透露给 UI 或 Controller,上层就会开始了解下层实现的细节。
因此在边界处,会转换成
- “无法连接到支付服务”
- “CSV 格式已损坏”
- “无法写入保存位置”
- “设备响应异常”
这类 在该层有意义的失败。
这里重要的是,翻译和记录日志并不是同一件事。如果只是翻译后向上抛出,通常不需要记录主日志。
3. 先看这张判断表
先用这张表确定大方向,会比较轻松。
| 位置 | 基本方针 | 主日志 | 主要职责 |
|---|---|---|---|
| helper / utility / private method | 原则上不大范围 catch |
不记录 | 通过 finally 收尾、局部回滚、必要最小限度的上下文补充 |
| Repository / Gateway / SDK 封装 | 只接收具体的异常类型 | 通常不记录 | 异常翻译、有限的 retry、连接或句柄的释放 |
| Application Service / UseCase | 将预期内的失败转换为结果 | 若在此处吞掉异常则视需要记录 | 定义失败单位、部分失败处理、用例单位的判断 |
| UI / Controller / API / Job / Message 边界 | 预期外异常的主要接收点 | 这里容易成为主日志记录点 | 面向用户的响应、HTTP 响应、继续处理下一条、是否中止的判断 |
| 未处理异常处理程序 / 宿主最终边界 | 防止漏记的最后一道防线 | Critical |
最终记录、flush、dump、退出与重启通道 |
画成图,大致是这样。
flowchart TD
A["发生异常"] --> B{"能否在此处决定 retry / 结果化 / 是否继续?"}
B -- "否" --> C["原则上不 catch,向上传递"]
B -- "是" --> D{"这里是层的边界吗?"}
D -- "否" --> E["仅进行局部 cleanup"]
D -- "是" --> F["必要时翻译为有意义的异常"]
E --> G{"在这里能明确失败单位和运维上下文吗?"}
F --> G
G -- "否" --> H["不记录主日志,向上层传递"]
G -- "是" --> I["记录一次主日志并决定响应"]
I --> J["必要时终止 / 重新初始化 / 继续下一条"]
这张图的重点有两个。
catch的首要理由是恢复或 cleanup,而不是记录日志- 记录日志的首要理由是运维上下文已经齐备,而不是发现了异常
4. 在调用层级的哪个位置做什么
4.1. 最深层的 helper / utility / private method
这里的基本原则是 不大范围接收异常。
例如字符串转换、解析、计算、内部格式整理、公共 helper 这类地方,
- 不知道是哪次界面操作
- 不知道是哪个 request
- 不知道这次失败是否只影响本次操作
- 不知道是否应该关闭整个界面
是无法判断的。
在这一层可以做的,主要是以下这些。
- 通过
finally释放资源 - 回滚中途已经破坏的局部状态
- 为异常消息补充最小限度的上下文
- 替换为更合适的异常类型
- 销毁已经无法再复用的对象
反过来,应该避免以下这类写法。
catch (Exception)后返回null/false/ 空数组- 在这里弹出
MessageBox - 在这里先记录
Error日志,再重新抛出 - 明明无法恢复原状,却”先继续再说”
尤其危险的是这种模式:中途已经改写了自己的状态却失败了,却仍然继续使用下去。 这种情况下,应当在当场就把状态恢复原状,或者,如果无法恢复,就以销毁该对象为前提处理,二者选其一。
4.2. 外部 I/O 边界:Repository / Gateway / SDK 封装
这是一个 catch 的理由很明确的层。
因为在这里,下层实现的具体细节会暴露出来。
- DB 驱动异常
- HTTP 通信异常
- 文件 I/O 异常
- COM / P/Invoke / vendor SDK 特有的异常
- 解析库或序列化器的异常
在这一层要做的事情,大致有 4 项。
-
接收具体的异常类型 不是接收宽泛的
Exception,而是接收有明确含义的具体异常。 -
翻译为有意义的失败 让上层不需要直接了解下层的实现细节。
- 如果要做局部 retry,就在这里做
不过,条件要设定得严格一些:
- 已确认是临时性失败
- 具备幂等性
- 已确定重试上限次数与等待方式
- 最终失败时的行为已经明确 只有这 4 个条件都满足时才可以进行 retry。
- 舍弃已损坏的连接或句柄 相比”继续用同一个对象接着用”,”重新创建连接”往往更安全。
这一层的日志方针,按以下思路思考会比较不容易动摇。
- 如果要向上重新抛出,通常不记录主日志
- 如果在这里把异常吞掉并转换为结果,那么此时应记录必要的日志或指标
- retry 过程中的每次尝试应控制在
Debug/Information/Warning的范围内,只对最终失败做较强的记录
这一层是 进行翻译的地方,通常不是 做出最终判断的地方。
4.3. Application Service / UseCase
这是决定”这次工作该以何种方式失败”的层。
例如,
- 保存处理
- 订单确认
- CSV 导入
- 批处理中的一条
- 一条消息的处理
这类 作为用例、具有整体性的单位,就存在于这一层。
在这一层,可以做出以下判断。
- validation 错误只让本次失败
NotFound相当于 404- 违反业务规则则等待用户修正
- CSV 中一行不合规则以
Warning记录并继续 - 外部服务临时故障则让整个处理失败
- 舍弃中途成果,从头重新开始
也就是说,这里是能够决定 失败单位 的地方。
这一层适合承担以下工作。
- 将预期内的失败转换为
Result或失败 DTO 的形式 - 汇总部分失败
- 决定允许多少条失败后仍继续处理
- 转换为错误代码或面向用户的消息 key
反过来,这一层不应该做的是,过多地引入 UI 展示或 HTTP 响应主体的组装逻辑。 在这一层,只需决定 作为用例的含义,把最终的展现方式交给边界层处理,这样更容易做到职责分离。
4.4. UI / HTTP / Job / Message 边界
在很多应用中,这里往往会成为 主日志记录点。
例如以下这些单位。
- WinForms / WPF 中一次”保存”按钮点击
- ASP.NET Core 中一个 HTTP request
- worker 处理的一条消息
- 批处理中的一条输入
- 一次定时执行的作业
这个位置了解以下信息。
- 是什么操作
- 是谁的操作
- 是第几条
- 是哪个 request / batch / message
- 失败时应向用户或调用方返回什么
因此,
- 在这里统一接收预期外的异常
- 带上下文记录一次主日志
- 转换为错误对话框、HTTP 500、Problem Details、作业失败、继续处理下一条等
这样的角色,容易落在这一层。
在这一层,重要的不是 大范围接收本身,而是 大范围接收之后,返回什么已经被明确定义。
例如在 batch 或 queue 中,分成两个阶段来考虑,会更清晰。
- 在单条边界处接收 决定是否只让这一条失败,继续处理下一条
- 父循环不要大范围吞掉异常 如果父循环崩溃,就倾向于让整个进程重启
“逐条失败并继续”和”父循环因预期外异常崩溃却悄悄地继续存活”,这两者完全不同。
4.5. 最后的未处理异常处理程序
这里是 最后的堡垒,不是什么神奇的恢复点。
具有代表性的大致有以下这些。
AppDomain.UnhandledException- WPF 的
Application.DispatcherUnhandledException - WinForms 的
Application.ThreadException - ASP.NET Core 的异常处理中间件或处理程序
- Generic Host / worker /
BackgroundService的最终异常处理
这一层的主要职责,大致也就这些。
- 最终日志记录
- flush
- dump 采集通道
- 保存会话信息或紧邻之前的上下文
- 整备退出代码或重启通道
反过来,也有一些不该对这一层抱有过高期待的地方。
- 一旦走到这里,往往说明上层设计存在遗漏
- 状态可能已经被破坏
- 有可能仍持有锁,进行繁重处理很危险
- 即便表面上还能继续运行,也不代表继续运行是安全的
关于 .NET 相关的实务注意事项,还有以下几点值得掌握。
AppDomain.UnhandledException是用于 通知和记录未处理异常 的事件。在其中加入过多恢复处理是危险的。- WPF 的
DispatcherUnhandledException虽然可以通过设置Handled = true让程序表面上继续运行,但 是否真的可以恢复 才是首先要判断的事情。 - WinForms 的
ThreadException在处理之后,应用也有可能进入 未知状态。 - ASP.NET Core 的异常处理中间件,需要放在 管道较靠前的位置,以便能够接收到后续的异常。
BackgroundService的未处理异常,在 .NET 6 及以后版本中会 被记录,并默认导致宿主停止。相比在父循环中把所有异常都吞掉,让宿主停止并交给重启策略处理,有时会更安全。
尤其是在桌面应用中,确实存在”捕获未处理异常后继续运行”的做法。 不过,能够继续运行 和 可以放心继续运行,是两件不同的事。
4.6. 用一条调用链来看
举个例子,考虑以下这样的流程。
flowchart LR
A["UI / Controller / Job 边界"] --> B["Application Service / UseCase"]
B --> C["Domain / 业务逻辑"]
C --> D["Repository / Gateway / SDK wrapper"]
D --> E["DB / HTTP / File / Vendor SDK"]
此时,角色大致会这样划分。
保存按钮 → SaveOrderUseCase → PaymentGateway → HTTP
PaymentGateway- 接收通信失败或响应格式异常
- 翻译为”支付服务连接失败”“支付服务响应异常”
- 如果要 retry,就在这里附带条件进行
- 如果要重新抛出,通常不记录主日志
SaveOrderUseCase- 将支付被拒等预期内的失败,转换为结果
- 视为”仅本次订单确认失败”
- 把失败结果整理成便于返回给 UI 或 API 的形式
- UI 按钮处理程序 / Controller
- 统一接收预期外的异常
- 附带
orderId、userId、requestId记录主日志 - 转换为对话框展示或 500 / 503 响应
- 未处理异常处理程序
- 只记录漏到这里的异常
- 执行 dump 或最终 flush
- 优先考虑退出通道,而不是恢复
采用这种划分方式,就会形成 技术细节在下层闭环,运维上下文在上层补充,判断在边界处进行 的格局。
5. 区分预期内的失败与预期外的异常
在这个主题上,最重要的是 不要把所有情况都当作同一种”异常”来处理。
先尝试这样划分。
| 失败类型 | 首先处理的位置 | 典型处理方式 |
|---|---|---|
| validation 不合规 | UseCase / request 边界 | 作为输入错误返回 |
NotFound / Conflict |
UseCase / Controller | 404 / 409 或界面消息 |
| 用户取消 / 关闭 | 操作边界 | 作为取消处理,通常不视为 Error |
| CSV 中一行不合规 | 单行边界 | 以 Warning 记录并继续 |
| 临时 timeout 最终导致失败 | I/O 边界 ~ request 边界 | retry 后仍失败则返回失败 |
NullReferenceException,前提被破坏 |
request / job 边界 | 记录主日志并返回失败响应 |
AccessViolationException、严重的 OutOfMemoryException、疑似 native 边界被破坏 |
最终边界 | 作为 Critical 处理,倾向于终止 |
预期内的失败是 可以在设计阶段就预先确定的失败。 预期外的异常则是 之后是否还能信任状态,值得怀疑的失败。
只要把这两者区分开,就能减少以下这类事故。
- 把
NotFound每次都当作Error - 把用户取消当作故障处理
- 把真正危险的前提破坏问题,用”仅本次失败”轻描淡写地放过
6. 日志应该在哪里、记录几次
在日志设计中,比 catch 的位置更重要的是先决定 由谁来记录主日志。
基本规则有 6 条。
- 针对一次失败,主要的
Error/Critical日志只记录一次 - 下层如有必要,只做 翻译和上下文补充
- 上层边界应记录 带有失败单位与运维上下文的主日志
- 只有在当场 吞掉异常的那一层,才承担该失败的记录责任
- 预期内的失败不要每次都记为
Error OperationCanceledException要与普通的故障日志区分开
把日志记录点大致整理成表格。
| 情况 | 主要记录位置 | 级别参考 | 补充 |
|---|---|---|---|
| validation 错误 | request / use case 边界 | Information 或不记录 |
不是故障,而是契约上的失败 |
| 用户取消 / shutdown | 操作边界 | Debug / Information |
通常不作为 Error |
| retry 过程中的临时失败 | 拥有 retry 逻辑的层 | Debug / Warning |
最终失败之前不要过度记录 |
| retry 用尽后仍失败 | request / job 边界,或当场吞掉异常的层 | Warning / Error |
附带失败单位记录 |
| 仅一行不合规仍继续 | item 边界 | Warning |
附带 fileId、rowNumber |
| 导致整个 request 失败的预期外异常 | request / UI / job 边界 | Error |
附带 requestId、userId、entityId |
| 进程级终止 | 未处理异常边界 | Critical |
flush、dump、重启通道 |
在实务中相当常见的,是以下这类重复日志。
- Repository 记录
Error - Service 对同一个异常再记录
Error - Controller 又记录一次
Error - 最后的未处理异常处理程序还记录了
Critical
这样一来,一次故障就会排列出好几份相同的堆栈跟踪。 对阅读的人来说,需要的不是 4 份相同的 stack trace,而是 一条主日志,加上必要时少量的辅助日志。
换句话说,基本原则是 日志记录一次,上下文按需补充。
7. 常见的 NG 做法
7.1. 在深层 catch (Exception) 后返回 null / false
这样容易丢失原因信息。 而且,调用方将无法区分”确实没有数据”还是”中途出现了故障”。
7.2. 每一层都记录 Error 日志后再重新抛出
这是最常见的重复日志的原因。
- 下层只做翻译
- 上层边界记录主日志
采用这种分工,可以大幅减少重复。
在 C# 中如果要重新抛出,基本原则是使用 throw;,以避免破坏堆栈跟踪。
7.3. 库层或公共组件直接输出 UI
如果公共组件直接弹出 MessageBox,或者直接决定 HTTP 响应正文,会破坏可复用性和职责分离。
下层最好只做到 返回有意义的失败 为止,这样更安全。
7.4. 把 OperationCanceledException 作为故障记录 Error 日志
取消是控制流程的一部分。
如果每次都记为 Error,真正的故障就会被掩盖。
7.5. 明明有外部副作用,却轻易进行 retry
发送邮件、扣款、设备指令、文件移动等操作,如果重复执行同一操作,很容易出事故。 只有在同时确认了 临时性失败 和 幂等性 时,才应该进行 retry。
7.6. 试图在最后的未处理异常处理程序中恢复一切
这里是最后的保险,不应该被放在设计的核心位置。
恢复策略最好放在更靠前的层,也就是 request / job / subsystem 的边界处,这样更安全。
8. 审查时的检查清单
在审查异常处理时,按以下顺序检查,会比较不容易漏掉问题。
- 能否用一句话说清楚这个
catch是 为了判断什么 而存在的 - 在这个位置,是否真的能够决定 retry / 结果化 / 是否继续 / 用户响应
- 如果在这里记录日志,上层是否会对同一个失败再记录一次
Error - 是否在边界处,把下层实现特有的异常翻译成了有意义的失败
- 中途被破坏的状态,能否在这里恢复;如果不能恢复,是否已经以销毁为前提
- 是否把
OperationCanceledException与普通故障区分开 - 是按 item 单位继续、按 request 单位失败,还是按 process 终止,这一点是否明确
- 对最后的未处理异常处理程序,是否只期待记录而不是恢复
- 日志中是否带有 requestId / userId / batchId / fileId / rowNumber 等失败单位的上下文
- 是否把”预期内的失败”和”前提被破坏”同等对待了
这份检查清单中特别有效的一点,是每次都用语言明确表达 “这个 catch 决定了什么”。
说不清楚这一点的 catch,大多是不必要的,或者位置太深了。
9. 简单的使用区分
最后,如果非常简短地总结,会得到这张表。
| 场景 | catch |
日志 | 错误处理 |
|---|---|---|---|
| helper / utility | 原则上不进行 | 不记录 | 不处理 |
| Repository / Gateway / SDK 封装 | 只接收具体的异常类型 | 通常不记录主日志 | 翻译、局部 retry、释放连接 |
| UseCase / Application Service | 接收预期内的失败 | 若吞掉异常则视需要记录 | 结果化、部分失败处理 |
| UI / Controller / request / item / job 边界 | 大范围接收预期外的异常 | 记录主日志 | 响应、消息、继续 / abort |
| 未处理异常处理程序 | 只处理漏到这里的 | Critical |
最终记录、退出通道 |
拿不准的时候,先记住以下这 5 点就足够了。
- 深层不要大范围捕获
- 在边界处接收
- 主日志只记录一次
- 吞掉异常的那一层承担责任
- 最后的未处理异常只负责记录和退出通道
10. 总结
异常处理并不是”哪里都能 catch,所以就到处 catch“这样的事情。
检查的顺序,大致按以下这样就足够了。
- 在这个位置真的能做出判断吗
- 在这里能明确失败单位吗
- 在这里能恢复状态,还是能重新构建状态
- 在这里记录日志会不会重复
- 这里是恢复点,还是最后的记录点
按这个顺序检查,梳理调用层级会变得相当容易。
尤其重要的是以下这 3 点。
- 深层主要负责翻译和 cleanup
- 边界主要负责判断和记录主日志
- 最后的未处理异常处理程序主要负责记录和退出通道
换句话说,基本原则是:在边界处接收异常,附加上下文,并只在能够恢复的地方进行处理。
一旦确立了这个原则,无论是代码审查还是故障调查,都会变得相当稳定,不容易出现偏差。
11. 参考资料
- .NET:异常最佳实践
- .NET:System.AppDomain.UnhandledException 事件
- WPF:Application.DispatcherUnhandledException 事件
- Windows Forms:Application.ThreadException 事件
- 处理 ASP.NET Core 中的错误
- ASP.NET Core 中间件
- 使用 BackgroundService 的 Windows 服务
12. 相关文章
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
意外异常发生时,应用该终止还是继续的判断表
本文从状态破坏、外部副作用、线程与原生边界等角度,整理发生意外异常时应用应该终止还是继续运行的判断依据,并给出常见场景下的判断表与建议。
Windows 应用崩溃时保留日志与转储的设计
整理当 Windows 应用因意外异常或程序缺陷而崩溃时,应如何组合使用常规日志、最终崩溃标记、WER LocalDumps 与监控进程,以便事后能够追溯原因。
Windows 应用程序开发安全性最低限度检查清单
本文以检查清单的形式,整理 WPF / WinForms / WinUI / C++ / C# 业务应用程序在权限、签名、更新、机密信息、HTTPS、输入验证、DLL 加载、日志等方面不想遗漏的基本要点。
如何理解 Windows 的会话隔离 ── Session 0・RDP・多用户同时运行
整理 Windows 应用程序开发者容易混淆的「会话」概念。说明服务无法显示 UI 的 Session 0 隔离原因、RDP 连接时会话的行为、命名对象的会话隔离,以及共享 PC・RDS 环境中常见的设计失误,从实务角度解说。
Windows 的进程间通信该怎么选 ── 命名管道 / TCP / gRPC / 共享内存 / COM 判断表
整理 Windows 应用程序之间应该如何选择通信方式。用判断表整理命名管道、本地 TCP、gRPC、共享内存、文件对接、COM 各自的优势与陷阱,并从实务角度说明 UI+服务分离・32bit/64bit 桥接・权限边界等常见架构,以及命名管道的实现示例。
常见问题
汇总了咨询这一主题时常见的问题。
- 应该在哪一层 catch 异常?
- 原则是不要在深层大范围 catch,而应该把捕获点靠近能够定义失败单位的边界。例如一次界面操作、一个 HTTP 请求、一个作业、一条消息这类处理边界,是比较自然的接收点。基本原则不是「哪里能 catch 就在哪里 catch」,而是在能够负责判断 retry、结果化、是否继续的地方进行捕获。在较深的 helper 或 utility 中,只应做 finally 中的收尾、局部回滚,以及异常的翻译,不宜更多。
- 异常日志应该在每一层都记录吗?
- 基本原则是:针对一次失败,主要的 Error / Critical 日志只记录一次。如果 Repository 记录 Error,Service 对同一个异常再记录 Error,Controller 又记录一次 Error,那么一次故障就会出现好几份相同的堆栈跟踪,让阅读的人感到困惑。下层应只做翻译和补充上下文,而在具备 requestId、userId 等运维上下文的上层边界记录主日志。只有把异常吞掉并转换为结果的那一层,才需要承担该次失败的记录责任。
- 预期内的失败和预期外的异常应该如何区分?
- 预期内的失败是可以在设计阶段就预先确定的失败,例如校验不通过或 NotFound,可以在用例(use case)单位上转换为结果,不必每次都记录 Error 日志。因用户取消而产生的 OperationCanceledException,通常也不当作 Error 处理。另一方面,像 NullReferenceException 这类前提被破坏的情况,应在 request / job 边界记录主日志并返回失败响应;AccessViolationException 或严重的 OutOfMemoryException 应作为 Critical 处理,并倾向于终止程序。只需区分这两类,就能减少真正危险的失败被掩盖的事故。
- 未处理异常处理程序中应该做什么?
- AppDomain.UnhandledException、WPF 的 DispatcherUnhandledException、WinForms 的 ThreadException 等,并不是恢复点,而是最后的记录点。它们的主要职责是最终日志记录、flush、dump 采集通道,以及退出代码或重启通道的整备。一旦泄漏到这一层,状态就可能已经损坏,即便表面上还能继续运行,也不代表可以安全地继续下去。恢复策略最好放在更靠前的 request 或 job 边界上处理。
作者简介
本文作者的个人简介页面。
Go Komura
小村软件有限公司 代表
以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。