タイトルは釣りで正しくは「.context(...)にメッセージだけを書くな」です1。
言いたいこと
anyhowやeyreを利用する際、中途半端な .context(...) や .with_context(|| ...) は二度と書かないでください。
- 途中で出現する 変数の中身を表示する 目的のみで使用されるべきです。
- 簡単なCLIツール等でユーザー向けにバックトレースもどきを出したいなら hooq クレートを併用してください
- 開発中でのデバッグ用途なら同じくhooqを使うか 素直に
RUST_BACKTRACE=1を使ってください - 大規模アプリケーションならば thiserror も使って丁寧にエラー型を定義し tracing エコシステムで丁寧にログを出すべきです
- こっちの話は本記事ではしません。後日頑張って書き起こす予定です
中途半端に自然言語のメッセージだけを表示する .context(...) にはもはや存在価値はありません。何がしたいの?
こんにちは。本記事は hooqアドベントカレンダー 5日目の記事です。
hooq というマクロクレートを作りました。 ? 演算子と式の間にメソッドをフックする属性マクロhooqを提供しています。アドカレではその関連事項をまとめています。
hooq作者的には次のコードがいただけません。(anyhow公式のExampleを少し改変)
use anyhow::{Context, Result, anyhow};
use std::fs;
use std::path::PathBuf;
pub struct ImportantThing {
locked: bool,
path: PathBuf,
}
impl ImportantThing {
pub fn detach(&mut self) -> Result<()> {
if !self.locked {
return Err(anyhow!("ImportantThing isn't locked!"));
}
self.locked = false;
Ok(())
}
}
pub fn do_it(mut it: ImportantThing) -> Result<Vec<u8>> {
it.detach().context("Failed to detach the important thing")?; // 許せない
let path = &it.path;
let content = fs::read(path)
.with_context(|| format!("Failed to read instrs from {}", path.display()))?;
Ok(content)
}
fn main() -> Result<()> {
let it = ImportantThing {
locked: true,
path: PathBuf::from("./src/main.rs"),
};
let _ = do_it(it).context("do_it was failed.")?; // 許せない
Ok(())
}
Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=3fcce36c80fa6549f5a6725ba44580c1
この中で許せるcontextメソッド呼び出しは path の中身を表示してくれる .with_context(|| format!("Failed to read instrs from {}", path.display()))? だけです。他はいたずらにコーディングの手間を増やし可読性を悪化させています。
何が一番問題かというと全く無意味な自然言語の補足を付けていることです。何コレ?
わかりやすい関数呼び出しをわざわざ自然言語にコンパイルする意味ありますか?
it.detach().context("Failed to detach the important thing")?; ですが、これは「 it.detach() が失敗した」以上の意味を表していません。
では最初から it.detach() is failed. と書けばいいんじゃないでしょうか?
もし軽量なCLIツールだったとして、このメッセージに遭遇したユーザーに何を求めるのでしょうか?
根本原因となったエラーのメッセージのみでは問題を解決できなかった場合、context / with_context 分として出力されたメッセージに基づいて、ユーザーはツールのGitHubに行くでしょう。そしてそこで検索窓を開き、その長ったらしい自然言語の文章で検索をかけるかと思います。
ならば最初から どんな関数 が どのファイル の どこ でエラーになったかを素直に書いた方が良いはずです。
あなたが頑張って考える自然言語の文章より、 元の式(関数呼び出し)の方が検索する上で多くの情報を与えてくれます。 もし失敗した関数が fs::read みたいな標準モジュールの関数やライブラリクレートの関数ならなおさらでしょう2。 docs.rs が最もエラー解決のためにヒントを与えてくれる存在になります。
つまり以下のように書いた方がマシで、筆者はそうしてきました。 そしてこれは今はもうhooqマクロが代わりにやってくれます。
it.detach().with_context(|| format!("it.detach() @ [{}:{}] is failed.", file!(), line!()))?;
Error: do_it was failed.
Caused by:
0: it.detach() @ [src/main.rs:23] is failed.
1: ImportantThing isn't locked!
hooqクレートを利用した例は最後に載せました。
コンテキストの"途中"には自然言語で表すべき処理はない
「エラー原因が明らかなので自然言語で書けるのだけど...?」と思ったかもしれません。確かにそういう場合もあるでしょう。
しかしそれはreturn Err(anyhow!("明らかなエラー原因はこれです")); 3 みたいに、 元となったエラー原因箇所で書かれるべき です。
トレースの途中が自然言語である必要性はまったくありません。その時の"コンテキスト"として何かしらの変数の値 (今回挙げた例なら path ) がどうなっているかを提示する用途では有用ですが、それならその変数(と変数名)のみ書けばよくて、頑張って書いた自然言語部分は特にデバッグには寄与しません。
.context(...) を挿入する目的で方針を決めてほしい
そもそも .context(...) がついていると何が嬉しいのでしょうか...?
それは自然言語のエラーメッセージが増えることではありません。以下のどちらかでしょう。
- 途中の変数の中身を表示できること
- バックトレースもどきが得られること
- 変数を表示しない場合でも付ける理由があればです
それならば以下のどちらかがおすすめです。
- 開発者自身がバックトレースを得たいなら、
RUST_BACKTRACE=1を付けて実行 すればよいです。- 公式にも例があります。変数の中身を表す目的での
context利用をともに使えます。
- 公式にも例があります。変数の中身を表す目的での
- ユーザーにもバックトレース的なものを見せてあげたいなら、 バックトレースもどきを作るべき です。
- なるべくすべての
?にcontext/with_contextを付与するべきでしょう - すべての呼び出しで行数がわかった方が便利です
- 開発者にとっても便利です
- なるべくすべての
context に自然言語のメッセージのみを与える使い方は、バックトレースもどきを与えるという目的に立った場合あまり冴えた方法ではありません。
つまり: hooqを使え!
バックトレースもどきを作るために context を手動で挿入するぐらいなら、hooq を使ってください! 使い方は、各関数の頭に #[hooq(anyhow)] を付けるだけです。
use anyhow::{Context, Result, anyhow};
use hooq::hooq;
use std::fs;
use std::path::PathBuf;
pub struct ImportantThing {
locked: bool,
path: PathBuf,
}
#[hooq(anyhow)]
impl ImportantThing {
pub fn detach(&mut self) -> Result<()> {
if !self.locked {
return Err(anyhow!("ImportantThing isn't locked!"));
}
self.locked = false;
Ok(())
}
}
#[hooq(anyhow)]
pub fn do_it(mut it: ImportantThing) -> Result<Vec<u8>> {
it.detach()?;
let path = &it.path;
let content = fs::read(path).with_context(|| format!("path: {}", path.display()))?;
Ok(content)
}
#[hooq(anyhow)]
fn main() -> Result<()> {
let it = ImportantThing {
locked: true,
path: PathBuf::from("./src/main.rs"),
};
let _ = do_it(it)?;
Ok(())
}
.with_context(|| ...) が自動付与され、エラー出力は次の通りになります。
Error: [src/main.rs:41:22]
41> do_it(it)?
|
Caused by:
0: [src/main.rs:26:16]
26> it.detach()?
|
1: [src/main.rs:15:13]
15> return Err(anyhow!("ImportantThing isn't locked!"))
|
2: ImportantThing isn't locked!
Error: [src/main.rs:41:22]
41> do_it(it)?
|
Caused by:
0: [src/main.rs:29:86]
29> fs::read(path).with_context(|| format!("path: {}", path.display()))?
|
1: path: ./not_found.txt
2: No such file or directory (os error 2)
29行目の let content = fs::read(path).with_context(|| format!("path: {}", path.display()))?; について、 変数(path)の中身はhooqがフックするメソッドからは知ることができない ので残してあります。.with_context(|| ...) は重ね掛けができるので、hooqがある場合でも変数の中身を表示するような context / with_context は有意義です!変数を表示する使い方だけはどの場合でも冴えてますね。
まとめ・所感
あんまり普段こういう強いタイトルや口調で記事は書かないのですが、最近ちょっと色々疲れた4ので今回憂さ晴らしのためにこういうタイトル・方向性の記事にしてみました...正直投稿するかも迷いました。不快に感じた方が居ましたら申し訳ないです。いつもだったら「anyhowを使うならhooqも一緒に使おう!」とか明るいタイトルにできたのですが闇が出てしまいました5。
hooqアドベントカレンダーと銘打っていますが、徐々にhooq以外のエラーハンドリング・エラーロギング周りの話を増やしていけたらなと思います。こんな私ですが今後ともよろしくお願いいたします。。。ここまで読んでいただきありがとうございました!
-
記事中で明記していませんが、記事で主張している通り変数の中身を表示する目的で使うなら (
format!を使う可能性が高いので遅延評価のために)with_contextを使うべき、となりそうです。その場合はタイトル詐欺ではない...? ↩ -
こう書いたのですがこれらの呼び出しは根本原因部分に来るはずなので、「途中」に関する話ではないですね。 ↩
-
hooqというOSSをほぼ丸1年をかけて業務外時間という貴重な時間を投げうって開発したものの、世間的には要らないOSSだったのだとわからせられ、感情が死にました。アドカレ記事全然いいねつかない... ↩
-
もっとぶっちゃけるとSEO的な意味でこういうタイトルでも一本書きたかったというのがあります。 ↩