在错误处理的架构设计中,用户提示、日志记录、State 更新这三者分别代表了对外沟通、对内回溯、系统一致性。
要平衡这三者,核心原则是: “State 为骨、日志为肉、用户提示为皮” 。
1. 平衡模型:职责分离
一个健壮的错误处理链条应当遵循以下分工:
| 维度 | 处理目标 | 触发时机 | 存储位置 |
|---|---|---|---|
| State 更新 | 逻辑一致性。确保 UI 状态(如按钮禁用、Loading 停止)与后端状态对齐。 | 立即(同步/异步首选) | 内存 (ViewModel/Store) |
| 日志记录 | 问题复盘。记录原始、未经过滤的底层错误(如 SQL 错误码、堆栈)。 | 捕获时(后台处理) | 控制台/磁盘/服务端 |
| 用户提示 | 情绪安抚与引导。将技术术语转化为人类可读的语言(如“网络拥塞”而非“Error 429”)。 | 仅在需要用户干预时 | UI 层 (Toast/Alert) |
2. 工程实践:统一拦截器模式
为了避免在每个 do-catch 块里写三遍逻辑,建议在架构层建立一个统一处理器。
A. 状态更新(State Update)
使用枚举状态机(State Machine)来驱动 UI。State 更新应该是最优先级的,因为它防止了用户的误操作。
Swift
// 防御点:进入失败状态的同时,必须关闭加载状态
self.state = .failure(error)
self.isLoading = false
B. 日志记录(Logging)
日志应当包含“上下文”。不要只记 error.localizedDescription,要记录:用户 ID、当前页面、失败的操作、原始错误码。
C. 用户提示(User Feedback)
基于错误分类进行差异化提示。
- 致命错误:弹窗(Alert),强制用户操作(如重新登录)。
- 非致命错误:轻量提示(Toast/Snackbar),不打断操作。
- 静默错误:不提示用户,仅后台记录日志(如预加载失败)。
3. 典型代码链路(TCA/MVVM 风格)
Swift
func handle(error: AppError) {
// 1. 记录日志 (对内)
Logger.error("API 失败", context: ["type": error.category, "trace": error.id])
// 2. 更新状态 (核心逻辑)
self.loadingState = .failed
self.isRetryEnabled = error.isRetryable
// 3. 用户提示 (对外)
if error.shouldNotifyUser {
NotificationManager.show(
title: error.userTitle,
message: error.userDescription,
style: error.severity == .critical ? .alert : .toast
)
}
}
4. 防御式策略:避免“过度提示”
在平衡三者时,最常见的错误是把日志直接展示给用户,或者UI 状态未更新但弹出了错误窗。
- 脱敏处理:永远不要在用户提示中显示
Optional(nil)或JSON parsing error。 - 状态保护:如果异步操作报错,必须确保 UI 按钮从“禁用”恢复到“可用”,否则用户会陷入死锁。
- 日志冗余:高频重试导致的错误不需要每次都记详细日志,可以使用“采样记录”或“合并记录”策略。
总结:评估矩阵
| 错误类型 | 用户提示 | 日志记录 | State 更新 |
|---|---|---|---|
| 断网/超时 | 高 (引导开启网络) | 中 (仅记录频率) | 重置按钮/显示空白页 |
| 后端逻辑错 (422) | 中 (告知输入有误) | 高 (排查前端验证漏洞) | 高亮输入框 |
| 系统崩溃 (500) | 低 (对不起,请重试) | 极高 (立即触发告警) | 返回上一级 |