public protocol SystemError: Error { // 开发者看的日志 var logMessage: String { get } // 用户看的友好提示 var userFriendlyMessage: String { get } // 底层错误,方便追踪 var underlyingErrors: [Error] { get } // 生成错误堆栈 func logMessageStack() -> String // 查找特定错误类型 func lookup<T: Error>(_ errorType: T.Type) -> T? // 便捷查找方法 func lookup<T: Error, V>(_ t: (T) -> V?) -> V? }核心思路很简单:
.underlyingErrors 保存错误链,方便找根因
extension SystemError { publicvar underlyingErrors: [Error] { [] } // 递归查找错误类型 publicfunc lookup<T: Error>(_ errorType: T.Type) -> T? { for error in underlyingErrors { iflet match = error as? T { return match } iflet match = (error as? SystemError)?.lookup(errorType) { return match } } returnnil } publicfunc lookup<T: Error, V>(_ t: (T) -> V?) -> V? { lookup(T.self).flatMap(t) } // 生成错误堆栈 publicfunc logMessageStack() -> String { format(error: self) } // 格式化错误树 privatefunc format(error: Error, prefix: String = "", isLast: Bool = true) -> String { let type = type(of: error) var message: String var underlyingErrors: [Error] switch error { caselet e asSystemError: message = e.logMessage underlyingErrors = e.underlyingErrors default: message = "\(error)" underlyingErrors = [] } let branch = prefix.isEmpty ? "" : (isLast ? "└─ " : "├─ ") var output = "\(prefix)\(branch)\(type): \"\(message)\"\n" let childPrefix = prefix + (isLast ? " " : "│ ") let childCount = underlyingErrors.count for (idx, error) in underlyingErrors.enumerated() { let lastChild = (idx == childCount - 1) output += format(error: error, prefix: childPrefix, isLast: lastChild) } return output } }最牛地是 format 方法,能把错误层次画成树状图,调试地时候超清楚。
struct MyCustomErrorStruct: SystemError { var logMessage: String var userFriendlyMessage: String var underlyingErrors: [any Error] }枚举方式:
enum MyCustomErrorEnum: SystemError { case ouch var logMessage: String { switchself { case .ouch: "哎呀,这里出问题了,需要更多调试信息..." } } // 堆代码 duidaima.com var userFriendlyMessage: String { switchself { case .ouch: "哎呀,出了点小问题" } } }Foundation 错误也能用
extension NSError: SystemError { public var logMessage: String { "\(domain):\(code) - \(localizedDescription)" } public var userFriendlyMessage: String { "\(localizedDescription)" } }DecodingError 也处理一下:
extension DecodingError: SystemError { publicvar logMessage: String { switchself { caselet .dataCorrupted(context): "数据损坏: \(context.logMessage)" caselet .keyNotFound(key, context): "找不到键: \(key) - \(context.logMessage)" caselet .typeMismatch(type, context): "类型不匹配: \(type) - \(context.logMessage)" caselet .valueNotFound(type, context): "找不到值: \(type) - \(context.logMessage)" default: "\(self)" } } publicvar userFriendlyMessage: String { "数据格式有问题,请稍后重试" } }typed throws 实战
enum UseCases { // 测试错误层次 staticfunc testHierarchy()throws(MyCustomErrorStruct) { throw .init( logMessage: "这是一个自定义错误结构体", userFriendlyMessage: "出了点问题", underlyingErrors: [MyCustomErrorEnum.ouch] ) } // 测试解码错误 staticfunc testDecodingError()throws(MyCustomErrorStruct) { do { let invalidJSON = "不是有效的-json" struct Foo: Decodable { var bar: String } let decoder = JSONDecoder() let_ = try decoder.decode( Foo.self, from: invalidJSON.data(using: .utf8)! ) } catch { let message = "抱歉,服务器开小差了" throw .init( logMessage: "无法解码 Foo 类型", userFriendlyMessage: message, underlyingErrors: [error] ) } } }新语法 throws(MyErrorType) 声明函数只抛出指定类型错误。好处是编译时就知道会抛啥,不用猜了。
do { tryUseCases.testHierarchy() } catchlet error asSystemError { // 给用户看的 print(error.userFriendlyMessage) // 给开发者调试用的 print("=== 错误堆栈 ===") print(error.logMessageStack()) } catch { print("\(error.localizedDescription)") }输出:
出了点问题 === 错误堆栈 === MyCustomErrorStruct: "这是一个自定义错误结构体" └─ MyCustomErrorEnum: "这里出问题了,需要更多调试信息..."复杂错误还能深入查找:
do { tryUseCases.testDecodingError() } catchlet error asSystemError { // 查找特定解码错误 iflet decodingError = error.lookup(DecodingError.self) { switch decodingError { case .dataCorrupted(let context): print("数据损坏: \(context.debugDescription)") default: print("不是数据损坏错误") } } print("=== 错误堆栈 ===") print(error.logMessageStack()) }为什么这样更好?
enum OldError: Error { case networkFailed case decodingFailed } // 用户看到:networkFailed // 开发者也看到:networkFailed // 没上下文,调试困难现在的方式:
struct NetworkError: SystemError { var logMessage: String = "HTTP 请求失败,状态码: 500" var userFriendlyMessage: String = "网络连接有问题,请检查网络设置" var underlyingErrors: [Error] = [URLError(.timedOut)] } // 用户看到:网络连接有问题,请检查网络设置 // 开发者看到:完整错误堆栈 // 有上下文,有层次,调试方便总结