返回文章
Rust

Rust CLI 的错误处理与可观测性

让错误不仅能够返回,还能帮助使用者行动:区分类型、补充上下文,并保留完整原因链。

一个工具失败时,使用者最需要的不是一段调试堆栈,而是三个答案:哪一步失败、为什么失败、接下来可以做什么。Rust 的 Result 提供了很好的基础,但仅仅把错误用问号一路传到 main,并不会自动得到一个好用的命令行程序。错误信息也需要被认真设计。

先区分预期失败与内部故障

用户输入无效、文件不存在、外部服务暂时不可用,都可能是正常运行中的预期失败。程序内部违反约定则属于另一类问题。把两者分开,才能决定退出码、重试策略和展示方式。库层使用有语义的错误类型,应用边界再把它转换成适合人的说明。

不要过早把所有错误变成字符串。字符串会丢掉类型信息,使上层只能通过匹配文字判断是否重试。保留原始原因,并在穿过模块边界时增加上下文,能够同时照顾程序处理与人工排查。错误链的每一层都应增加新的信息,而不是重复一句“操作失败”。

在真正理解操作的地方补充上下文

use std::{fs, io, path::Path}; fn read_config(path: &Path) -> Result<String, io::Error> { fs::read_to_string(path).map_err(|cause| { io::Error::new( cause.kind(), format!("cannot read config {}: {cause}", path.display()), ) }) } fn main() { if let Err(error) = read_config(Path::new("config.toml")) { eprintln!("配置读取失败:{error}"); std::process::exit(1); } }

这个简化例子保留了 IO 错误的类别,并指出具体配置路径。真实工程中可以用自定义错误枚举保留完整的 source 链,或者在应用层采用上下文工具。关键不在于选择哪个库,而在于错误发生时是否仍然知道操作对象和所在阶段。敏感路径或令牌则需要在输出前脱敏。

让标准输出保持可组合

成功结果写到 stdout,进度与诊断写到 stderr,这样工具就能被管道组合。面向人的默认输出应当简洁;详细日志通过显式开关启用。需要机器处理时,提供稳定的结构化格式,并把字段契约当作公共接口,不要在同一输出流里混入装饰性文本。

把失败体验也放进测试

为缺失文件、无效参数、无权限目录和下游超时建立测试。除了验证退出码,也检查关键说明是否存在,标准输出是否仍然保持干净。对完整错误文案做快照时应当谨慎,临时目录等环境差异会让测试脆弱。断言稳定的语义信息,通常更值得。

一个可观察的 CLI 不一定需要复杂的日志系统。明确的输出通道、保留下来的原因链和可以预测的退出行为,就能让它在脚本、终端和自动化流水线中都更可靠。

感谢阅读,希望这些思考对你有所启发。