Rust 错误类型设计:库错误要能被上层恢复
Rust 错误类型设计:库错误要能被上层恢复
一、错误不是只为了打印
Rust 的Result很适合表达失败,但错误类型设计不好,上层仍然很难处理。很多库直接返回字符串错误,调用方只能打印,无法判断是重试、提示用户,还是终止程序。
错误类型的价值,是把失败语义交给上层。库代码应该提供可匹配的错误种类,并保留必要上下文。应用层再决定日志、重试和用户提示。
二、错误要分层
flowchart TD A[IO 错误] --> D[库错误] B[解析错误] --> D C[业务校验错误] --> D D --> E[应用处理]底层错误包括 IO、网络、解析、权限等。库层可以把它们包装成自己的错误类型,但不要丢失来源。应用层需要知道失败大类。
例如配置文件不存在和配置格式错误,处理方式不同。前者可以提示创建配置,后者要提示修正格式。字符串错误无法稳定区分。
三、枚举适合表达可恢复错误
#[derive(Debug)] pub enum ConfigError { NotFound, InvalidFormat(String), Io(std::io::Error), } pub fn load_config(path: &str) -> Result<String, ConfigError> { std::fs::read_to_string(path).map_err(ConfigError::Io) }真实项目可以使用thiserror简化实现。关键是错误类型要让调用方能够 match,而不是只能看字符串。
match load_config("app.toml") { Ok(cfg) => println!("{cfg}"), Err(ConfigError::NotFound) => eprintln!("请先创建配置文件"), Err(err) => eprintln!("配置加载失败: {err:?}"), }应用层决定如何展示错误。库层不要直接打印,也不要直接退出进程。
四、上下文要适量保留
错误需要上下文,但不能把敏感信息塞进去。路径可以保留相对路径,Token 不应进入错误信息。AI 工具里尤其要注意,模型请求错误可能包含用户输入摘要。
还要区分可恢复和不可恢复。配置缺失、网络超时、模型限流都可能恢复;内部状态损坏、协议不兼容可能需要终止。错误类型应体现这种差异。
错误链也要保留。上层看到ConfigError::Io时,最好仍能访问底层std::io::Error。这样日志里可以包含系统错误码,用户提示则保持简洁。面向人和面向排障的错误信息不必完全一样。
anyhow和thiserror适用场景也不同。应用入口可以用anyhow快速附加上下文,库接口更适合暴露明确 enum。库代码如果返回anyhow::Error,调用方就很难细分处理。
还要为错误写测试。触发配置缺失、格式错误、权限不足,确认返回的 variant 正确。错误路径不测试,后续重构很容易把可恢复错误变成普通字符串。
最后,错误文案要稳定。CLI 用户和脚本可能依赖错误码或退出码,不要随意改变语义。
退出码也应分层。配置错误、用户输入错误、外部服务错误、内部错误可以对应不同退出码区间。这样脚本能做自动处理,而不是只能判断成功或失败。
错误上下文要用source串起来。比如读取配置失败,底层可能是权限不足或文件不存在。上层错误说明“配置加载失败”,底层 source 保留系统原因。两层信息都重要。
还要避免过度包装。每一层都加一段冗长上下文,最后错误信息会像套娃。只在跨越抽象边界时补充有价值的信息即可。
最后,错误处理规范应写进贡献文档。多人协作时,错误风格不统一,会让 CLI 体验割裂。
还要警惕错误信息里泄漏敏感数据。有一次日志里打印了完整的 API 响应体,其中包含了用户的手机号和请求原文。后来在错误类型里加了"脱敏"方法,自动把已知的敏感字段替换为[REDACTED]。错误信息既要帮助排查,也要守住隐私底线。
五、总结
Rust 错误类型设计要分层表达失败语义,让上层能 match、恢复、重试或提示用户。库代码不要直接打印和退出。
错误不是最后一行日志。好的错误类型,是系统把失败处理权交给调用方的接口。