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 回の再現になる。

← 記事一覧に戻る

コメント

…