Backtraceの話から始まり、前回までを通してエラー型の基本知識を確認してきました。
今回はこれら基礎知識を踏まえた上で、 anyhow クレートのドキュメントを読み直し、まとめたいと思います。
anyhow系統のクレート1は、「特に細かくエラーを分類せず、とにかく雑に伝搬させたい」という時に使うものでしょう。前回最後に「(雑に)何かしらエラーを表すトレイトオブジェクト」として Box<dyn std::error::Error> を紹介し、 Result<(), Box<dyn std::error::Error>> を main 関数の最後の返り値型に使うという話をしました。anyhow::Errorの説明を読んだ感じ、 "Error works a lot like Box<dyn std::error::Error>" とあるので今回はこの話の続きと捉えてよさそうです!
anyhow
トップページに紹介されている機能をまとめてみました!
-
anyhow::Error型の提供 -
anyhow::Contextトレイトによるanyhow::Errorへのコンテキスト付与 -
anyhow::Errorから元のエラーなどへのダウンキャスト - バックトレース(
std::backtrace::Backtrace)の提供 - ユーティリティマクロ(
anyhow!やbail!等)の提供
各機能を軽く見ていきます!
anyhow::Error 型の提供
色々な機能が施された Box<dyn std::error::Error> の上位互換となる型です。
Box<dyn std::error::Error> も同様ですが、 std::error::Errorトレイトを実装している任意の型を内包することができます。
これの何が嬉しいかというと、ある関数の返り値型が Result<T, anyhow::Error> のようである時、 ? 演算子には From::from による型変換機能が内蔵されているため、 std::error::Error さえ実装されているエラーならば2テキトーに ? を付けることですべて同一に扱うことができます!
fn main() -> Result<(), anyhow::Error> { // anyhow::Result<()> で良いがわかりやすさのため分けて表現
let () = match 334 {
0 => Err(anyhow::anyhow!("= 0"))?, // anyhow::Error 自体
n @ 1..334 => {
#[derive(thiserror::Error, Debug)]
#[error("{0} < 334")]
struct UnderError(usize);
Err(UnderError(n))? // std::error::Error を実装した構造体
},
n @ 334.. => {
Err(std::io::Error::other(format!("334 <= {n}")))? // 外部クレートのエラー
}
};
Ok(())
}
上記の Err(...) において Err に包まれている型はすべて異なりますが、その差を意識することなく ? 演算子を利用できるわけです。
anyhow::Error のメソッド
クレートの中心に位置する型なだけあり、 anyhow::Error は結構メソッドが多いためここで軽くまとめておこうと思います。似た役割のメソッドは同じ行にまとめました。
| メソッド | 説明 |
|---|---|
new |
anyhow::Error を生成する。内包されるエラー型が Backtrace をprovideしない時、内部で Backtrace が作成されキャプチャされる |
msg |
これに Display + Debug な型の値を与えるとそれをメッセージとして anyhow::Error が生成される。ただしバックトレース等は保存されない模様(保存したい場合は new を使う)。 anyhow::anyhow! マクロとほぼ等価だがこちらは関数として扱える( .map の引数に直接渡したりなどできる )という利便性がある |
backtrace |
anyhow::Error が持つ std::backtrace::Backtrace の参照を取得 |
chain / root_cause
|
元となるエラーや Context のメソッドにより途中に含められた値のチェインを得る。 root_cause はその中でも一番最初の元となるエラーを返す |
downcast / downcast_ref / downcast_mut
|
Anyにあるそれらと同様、内包している元となったエラーや Context のメソッドにより途中に含められた値へのダウンキャストを提供する。 |
is |
こちらも Any 同様内包している元となったエラーやContext のメソッドにより途中に含められた値へとダウンキャスト可能かどうかを示す |
context |
Context::context を Error にも生やしたもの。 なぜ .with_context(...) はないのだろう...?
|
from_boxed / into_boxed_dyn_error
|
Box<dyn std::error::Error> との相互変換を行うための関連関数・メソッド |
reallocate_into_boxed_dyn_error_without_backtrace |
into_boxed_dyn_error と似ているがこちらはバックトレースが失効される模様 |
本当は試したことがないメソッドが色々あるので試したいところなのですが、色々忙しいので今回は見送りで...ただ見た感じユーザーが普段使いするものは少なく、エラーライブラリを作ったりする際に便利なものという位置づけのようですね。ユーザーが普段使いすることがあるメソッドは new ぐらいではないでしょうか?
anyhow::Context トレイトによる anyhow::Error へのコンテキスト付与
anyhow名物 anyhow::Context は、 Result 型や Option 型に .context(...) (引数即時評価)メソッドや .with_context(|| ...) (引数遅延評価) メソッドを提供します。これらのメソッドを利用することでエラーに背景を付与できます!
ちょっとしたハックなのですが、line!() マクロ (や複数ファイルに分かれるなら file!() マクロ) を利用し、すべての ? に .with_context(|| ...) を挟めるとエラートレースもどきが得られます。
use anyhow::Context;
fn func1() -> anyhow::Result<()> {
Err(anyhow::anyhow!("error!"))
}
fn func2() -> anyhow::Result<()> {
func1().with_context(|| line!())?;
Ok(())
}
fn func3() -> anyhow::Result<()> {
func2().with_context(|| line!())?;
Ok(())
}
fn main() -> anyhow::Result<()> {
func3().with_context(|| line!())?;
Ok(())
}
Error: 20
Caused by:
0: 14
1: 8
2: error!
std::backtrace::Backtrace があんまり好きくなかった筆者はこの方法を多用していました。
そしてこれがhooq属性マクロ誕生のきっかけだったりします。
詳しくは次の記事を読んでください
【Rust】.context(...)を書くな【anyhow・eyre】 #hooq - Qiita
anyhow::Error から元のエラーなどへのダウンキャスト
Any トレイトと似た感じで、 anyhow::Error の元になったエラーや、途中で付与したコンテキストへダウンキャストする機能が用意されています。
// マスキング(reduction)によりエラーが返っている場合は、
// オリジナルのコンテンツではなく代わりのコンテンツを返す
match root_cause.downcast_ref::<DataStoreError>() {
Some(DataStoreError::Censored(_)) => Ok(Poll::Ready(REDACTED_CONTENT)),
None => Err(error),
}
(公式ドキュメントより引用)
この match について、「anyhow::Error がトレイトオブジェクトである」点、そして「エラー型について動的ディスパッチ的に分岐している」点に注目すると、良くあるGCありクラスベースオブジェクト指向言語(Javaとか?)のtry-catchなどに近い仕組みに見えますね。網羅性を保てないためRustではこのハンドリング方法は微妙ですが、知っておくと何かに応用できるかもしれません。
バックトレース( std::backtrace::Backtrace )の提供
Box<dyn std::error::Error> からの最大の優位性はこの Backtrace が得られることでしょう。
RUST_BACKTRACE=1 や RUST_LIB_BACKTRACE=1 を設定して実行すると、 anyhow::Error 生成時点から勝手にバックトレースを生成してキャプチャしてくれます。
元となるエラーが Backtrace をprovideしない場合、そこまでのバックトレースは存在しないことに気を付けましょう。provideにより anyhow::Error にそれまでのバックトレースを反映させる例は前回地味に記載しているので参考にしてみてください。( func5 → func4 → func1 の例)
RUST_BACKTRACE=1 と RUST_LIB_BACKTRACE=1 の使い分け・工夫
Backtrace解説回 でも言及しましたが(なぜか)anyhowクレートのドキュメントの方に同内容の紹介がありました!
- パニック時・
Resultによるエラーハンドリング時の両方でバックトレースが得たい:RUST_BACKTRACE=1 -
Resultによるエラーハンドリング時だけ欲しい:RUST_LIB_BACKTRACE=1 - パニック時だけ欲しい:
RUST_BACKTRACE=1かつRUST_LIB_BACKTRACE=0
このようなスイッチングのために環境変数が2つ用意されています。
ユーティリティマクロの提供
楽に anyhow::Error を作ったりするためのユーティリティマクロがいくつか用意されています。軽く紹介!
-
anyhow!: 文字列スライス (&str) から楽にanyhow::Errorを作れるマクロです。format!マクロのようなフォーマット機能あり -
bail!:return Err(anyhow!(...));のショートハンドとなるマクロです。慣れると便利 -
ensure!:assert!マクロの親戚といった感じでしょうか?失敗時はパニックする代わりにanyhow::ResultのErrを返します。
use anyhow::{anyhow, bail, ensure};
fn f(n: usize) -> anyhow::Result<()> {
if n == 0 {
return Err(anyhow!("0はダメ"));
}
if n == 4 {
bail!("{n}は不吉!");
}
ensure!(n != 666);
Ok(())
}
fn main() {
println!("{:?} {:?} {:?} {:?}", f(0), f(4), f(666), f(42));
}
Err(0はダメ) Err(4は不吉!) Err(Condition failed: `n != 666` (666 vs 666)) Ok(())
Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=cc0da5bf476112e0ac52008bb4bdbc98
まとめ・所感
というわけで、hooqアドベントカレンダー 22日目の記事でした!
hooqはメソッドを ? 演算子にフックする属性マクロです。本アドカレではhooqの使い方を始め、hooqマクロを作成するにあたり得た知識、Rustのエラーハンドリング・エラーロギング周りの話をまとめています。
hooqマクロはanyhowクレートや次回取り組むeyreクレートとの相性も良いということは以下の記事にまとめています!もし良かったら読んでみてください ![]()
- 【Rust】anyhowで実行時エラー発生行やスタックトレース的なものを取得する方法2選 #hooq - Qiita
- 【Rust】.context(...)を書くな【anyhow・eyre】 #hooq - Qiita
- color-eyre × hooq = 💪 #Rust - Qiita