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 行缺少 = 可以。路径、行号、键名、上游状态码 —— 这些是你手里有、但读者拿不到的信息。

错误消息里省下的每一个字,都变成事后的一次复现。

← 返回文章列表

评论

…