Rust の Result:コンテキストは失敗した行に付ける
`?` はエラーをそのまま上へ投げ、どの段階で失敗したかを伝えません。中核は照合できる構造化エラー、アプリ層は Context で「何をしていたか」と入力を添える。
? は便利で、何もしていないことを忘れがちです。実際には Err を上へ投げ直すだけで、呼び出しの積み重ねは途中で落ちます。ログに残るのが No such file or directory だけ、ということが起きます。どのファイルを、誰が読もうとしたのか。
2 層に分ける
エラーは 2 層に分けています。
| 層 | エラー型 | 読み手 |
|---|---|---|
| ライブラリ / 中核ロジック | enum(thiserror) |
分岐のために match する呼び出し側 |
| アプリ / サービス層 | anyhow::Error + Context |
ログを読む人間 |
中核で enum を使うのは、呼び出し側が本当に区別したい場合があるからです。再試行、縮退、別のステータスコード。ひとたび 1 リクエストの処理に入れば誰も 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)
}
2 つの with_context の違いは、ファイルが読めなかったのか、読めたが内容が不正だったのかです。どちらかを落とすと、ログには片方しか出ません。
エラーを String に落とさない
map_err(|e| e.to_string()) は単純化に見えて、分類する能力を捨てています。呼び出し側が受け取るのは文字列で、できるのは表示だけです。さらにエラーの連鎖が切れ、source() で根本原因をたどれなくなります。anyhow は置き換えではなく包むので、連鎖が残ります。
メッセージに入力を載せる
良いエラーメッセージの基準は素朴です。デバッガのない人がこの 1 行だけで次に何を調べるか分かるか。failed to parse config は不合格で、解析配置 /etc/app/conf.toml: 第 12 行缺少 = は合格です。パス、行番号、キー名、上流のステータスコード。手元にあって読み手にない情報を載せます。
エラーメッセージで省いた 1 文字は、あとで 1 回の再現になる。

コメント
…