rust: anyhow::Context はどう使うとよいだろう
Rust でエラーの内容を出すのに anyhow::Context を使っていたけど、tracing_subscriber というものがあることを知った。
まじめにログを出すなら tracing_subscriber を使わないと制御が面倒だし、anyhow::Context は簡易ログ的なものだろうか?と思った。
いま anyhow::Context で書いていたときのコードを見直していて、どうしたもんだかと悩んでいるところだ。
.context() と .with_context(||) の評価タイミングの違い
.with_context() の中身はクロージャーなのだが引数を取らない。
だったらクロージャーじゃなくていいやん!と思って聞いてみると Err のときだけ評価するんだとか。
ほんとうかな?
AI にだまされ慣れてきたので確認できないものは疑うようになってしまった。
コンパイルしてエラーになるようなものだとわかりやすくてよいのだけどね。
ユーザ向け?
ユーザのリクエストに対してエラーを見せるのによいんじゃないか、というものだ。
ただ、ちゃんとやることになると文言の多国語対応とかも発生するし、そうなると文字列で返すのは最終的に面倒だ。
もしユーザの入力に対して起きたのだったとしても、その文字列を下の方から返していくことはなかろう。
anyhow::bail!()もそうだ
anyhow::Result<> を使っていて、エラーと判定したので何かエラー返さないといけないとき、何も考えずに anyhow::bail!() を使っていたのだが、
これも同じことが言える。
golang が fmt.Errorf() だったり errors.New() だったりが文字列なので、整数で扱うことはできないのかなあと気になったものだ。
Rust は特にそういう制限はないはずだ。
必要になるまで使わなくてよい気がする
などなど考えると、がんばって .context() をつけなくてもよい、と思う。
tracing::error!() などを書いていった方がデバッグには役立つだろう。
調べ事
log::*
tracing-subscribe のことを書いたが、標準みたいな rust-lang/log クレートもある。
これ自体はログシステムではなく API のインターフェースと空実装だけ提供しているらしい。
出力したい場合は自分で書くかクレートを持ってくるかするそうだ。
env_logger を試してみた(use しなくても Cargo.toml にあるやつは使えるのね。。。)。
use std::{result::Result, str};
use log::*;
#[derive(Debug, PartialEq)]
struct MyError { reason: &'static str }
impl MyError {
pub const TOO_SMALL: Self = Self{ reason: "too small" };
pub const TOO_LARGE: Self = Self{ reason: "too large" };
}
fn hello(value: i32) -> Result<bool, MyError> {
if value < 2 {
Err(MyError::TOO_SMALL)
} else if value < 5 {
Ok(true)
} else {
Err(MyError::TOO_LARGE)
}
}
fn main() {
env_logger::init();
for i in 0..8 {
match hello(i) {
Ok(v) => println!("{v}"),
Err(MyError::TOO_SMALL) => error!("too small!"),
Err(MyError::TOO_LARGE) => error!("too large!"),
Err(e) => error!("{:?}", e),
}
}
}
RUST_LOG を指定しないと ERROR レベルの出力になった。
$ cargo run
[2025-12-07T13:17:11Z ERROR hello] too small!
[2025-12-07T13:17:11Z ERROR hello] too small!
true
true
true
[2025-12-07T13:17:11Z ERROR hello] too large!
[2025-12-07T13:17:11Z ERROR hello] too large!
[2025-12-07T13:17:11Z ERROR hello] too large!
$ RUST_LOG=none cargo run
true
true
true
なので use tracing::* でログを書くよりは use log::* で書いておいてバイナリクレートにお任せするのがよいのだろう。
と、置き換えてみたのだが、作法悪く書いたところは use だけ置き換えるとエラーになってしまった。
どうお作法が悪かったかというと、こんな書き方だ。
let msg = "ほげほげ";
error!(msg);
anyhow::bail!(msg)
error!("{}", msg) にせい、ということだった。
C言語で "%s" を使いなさい、というのと同じような話だろう。
まあ、私も書いたときに気になっていたからね。
Err()
anyhow::Result<> を使いたくなるのは、std::io::Error や std::string::ParseError のように違う型を扱いたいからだ。
オリジナル?は std::result::Result<T, E> のはずだ。
io::Error は struct で、string::ParseError は type だった。
しかし io には IntoInnerError のように別の struct のものもある。
ファイルが std/io/buffered/mod.rs なのでモジュールが別になっているものがそうなっている?
まだ学習不足だ。
ここは io::Error のまねをしてみた。
use std::result::Result;
#[derive(Debug, PartialEq)]
enum MyErrorKind {
TooSmall,
TooLarge,
}
#[derive(Debug, PartialEq)]
struct MyError { kind: MyErrorKind }
impl MyError {
pub const TOO_SMALL: Self = Self{ kind: MyErrorKind::TooSmall };
pub const TOO_LARGE: Self = Self{ kind: MyErrorKind::TooLarge };
}
fn hello(value: i32) -> Result<bool, MyError> {
if value < 2 {
Err(MyError::TOO_SMALL)
} else if value < 5 {
Ok(true)
} else {
Err(MyError::TOO_LARGE)
}
}
fn main() {
for i in 0..8 {
match hello(i) {
Ok(v) => println!("{v}"),
Err(MyError::TOO_SMALL) => eprintln!("too small!"),
Err(MyError::TOO_LARGE) => eprintln!("too large!"),
}
}
}
区別できれば良いだけなら kind ではなくて文字列でもよいと思う。
ただ文字列にすると match でカバーできていないかもということで何でも判定が必要になった。
use std::{result::Result, str};
#[derive(Debug, PartialEq)]
struct MyError { reason: &'static str }
impl MyError {
pub const TOO_SMALL: Self = Self{ reason: "too small" };
pub const TOO_LARGE: Self = Self{ reason: "too large" };
}
fn hello(value: i32) -> Result<bool, MyError> {
if value < 2 {
Err(MyError::TOO_SMALL)
} else if value < 5 {
Ok(true)
} else {
Err(MyError::TOO_LARGE)
}
}
fn main() {
for i in 0..8 {
match hello(i) {
Ok(v) => println!("{v}"),
Err(MyError::TOO_SMALL) => eprintln!("too small!"),
Err(MyError::TOO_LARGE) => eprintln!("too large!"),
Err(e) => eprintln!("{:?}", e),
}
}
}
自動にはできんのか
こう、うまいことやってくれることが多い Rust だから、
Result<> を返すところで ? を使っていたら自動的に error!() を埋め込んでくれるような技があるかもしれない、と期待した。
なかった。
? は Sugar Syntax
すべての戻り値に関する書き方は match を使った文に置き換えられる。
なら全部 match で書いてもいいやん、と思うが 慣例的なRustコード(Idiomatic Rust)というのがあって
なるべくそっちを使う方が好まれる傾向にあるようだ。
tracing は別物らしい
関数単位でなら #[tracing::instrument(err)] が使えるというのはよい情報だった。
何が出るんだろうね?
サンプルコードも作ってもらった。
#[tracing::instrument(err)]
fn f(x: i32) -> Result<(), &'static str> {
if x < 0 {
return Err("negative");
}
Err("still error")
}
fn main() {
tracing_subscriber::fmt().init();
f(-1).unwrap();
.......
ログはこう。
どの関数でエラーになっているか分からないけどいちいちエラーになりそうな箇所に埋め込むのは大変すぎる、
というときに使えるかな。
$ RUST_LOG=trace cargo run
2025-12-08T14:08:47.186772Z ERROR f{x=-1}: hello: error=negative
そして、これは log ではなく tracing の方だ。
はー、なんでもかんでも tokio-rs の世話になりっぱなしやねー。
inspect_err
本命はこれだろう。
.with_context(|| format!({:?}, e))? ではなく .inspect_err(|e| error!("{:?}", e))? のように書く。
先ほどの f() だけ置き換えた。
fn f(x: i32) -> Result<(), &'static str> {
if x < 0 {
return Err("negative").inspect_err(|e| error!("{:?}", e))?;
}
Err("still error").inspect_err(|e| error!("{:?}", e))?
}
ログはこう。
.with_context() などを使うよりはこっちの方がいいかな。
$ RUST_LOG=trace cargo run
2025-12-08T14:42:08.283749Z ERROR hello: "negative"
anyhow::bail!() はエラー値そのものになるので、anyhow::Result<> のエラーをちょっと返したいときなどか。
main.rs しか使わないようなアプリだったらそんなに深く考えなくても良いだろうし、結局は場合によりけりか。
とはいっても、深く考えるときのことも知っておかねばと思う今日この頃。