Rust 的 Result 里,上下文要补在出错的那一层
`?` 会把错误原样上抛,不告诉你在哪一步失败。底层返回可匹配的结构化错误,应用层用 Context 补上「在做什么」和输入参数,日志里才有能定位的那一行。
? 很好用,好用到容易忘记它其实什么都没做:只是把 Err 往上抛。调用栈是倒着丢的,等你看到日志时,剩下的可能只是一句 No such file or directory —— 哪个文件?谁在读?
两层结构
我的做法是把错误分成两层:
| 层 | 错误类型 | 给谁看 |
|---|---|---|
| 库 / 核心逻辑 | enum(thiserror) |
调用者,需要 match 决定分支 |
| 应用 / 服务层 | anyhow::Error + Context |
人,日志与报错信息 |
核心逻辑里用 enum 是因为调用者可能真的需要区分(重试、降级、返回不同状态码)。而一旦进入「处理一次请求」这一层,就没有人再 match 了,需要的是一条能定位的记录。
在出错的那个调用点补
Context 要写在真正可能失败的那一行,而不是函数入口:
use anyhow::{Context, Result};
pub fn load_config(path: &Path) -> Result<Config> {
let text = fs::read_to_string(path)
.with_context(|| format!("读取配置 {}", path.display()))?;
let config: Config = toml::from_str(&text)
.with_context(|| format!("解析配置 {}", path.display()))?;
Ok(config)
}
两条 with_context 的区别正是一个文件读不进来、和文件读进来了但内容不合法。少任何一条,日志里就只剩其中一个。
别把错误降级成 String
map_err(|e| e.to_string()) 看着像简化,实际是把分类能力丢掉了:调用者拿到的是一段文本,只能打印。它还有个副作用 —— 错误链断了,source() 再也追不到底层原因。用 anyhow 也保留链:Context 是包装而不是替换。
消息里要带输入
判断一条错误消息好不好,标准很朴素:没有调试器的人能不能只看这一行知道下一步查什么。failed to parse config 不行,解析配置 /etc/app/conf.toml: 第 12 行缺少 = 可以。路径、行号、键名、上游状态码 —— 这些是你手里有、但读者拿不到的信息。
错误消息里省下的每一个字,都变成事后的一次复现。

评论
…