anyhow
はエラー生成が便利で、開発速度にメリットがあるため初動で使われることが多いです。しかし、Result
のErr
に anyhow::Error
を指定して露出させてしまうと、将来的な変更がしにくくなります。例えば、anyhow
をやめてカスタムエラーに変更したくなったときにインターフェースの互換性が失われる恐れがあります。この問題を解決するために、thiserror
の#[error(transparent)]
を用いてanyhow
を隠蔽する方法を紹介します。これにより、anyhow
のエラー生成の手軽さと、将来の互換性を壊しにくさを両立できます。
まず、Cargo.toml
に以下の依存関係を追加します。
[dependencies]
thiserror = "1.0.40"
anyhow = { version = "1.0.70", features = ["backtrace", "std"] }
anyhow
の backtrace
と std
のfeaturesを有効にすることで、バックトレース情報をサポートし、エラーハンドリングで標準ライブラリのエラーも扱えるようになります。
以下のサンプルコードでは、thiserror
を用いて MyError
構造体を定義し、その中に anyhow::Error
を隠蔽しています。
fn main() {
let err: Result<(), MyError> = stack1();
eprintln!("{err:?}");
}
#[derive(Debug, thiserror::Error)]
#[error(transparent)]
struct MyError(anyhow::Error);
fn stack1() -> Result<(), MyError> {
stack2()?;
Ok(())
}
fn stack2() -> Result<(), MyError> {
parse_int()?;
Ok(())
}
fn parse_int() -> Result<i32, MyError> {
"invalid"
.parse::<i32>()
.map_err(|e| e.into()) // ParseIntErrorをanyhow::Errorに変換する。このタイミングでバックトレースが付与される。
.map_err(MyError) // anyhow::ErrorをMyErrorに変換する
}
プログラムを実行するために、以下のコマンドを使います。
RUST_BACKTRACE=1 cargo run
実行結果は以下のようになります。
Err(MyError(invalid digit found in string
Stack backtrace:
...
6: my_crate::parse_int::{{closure}}
at ./src/main.rs:23:22
...
8: my_crate::parse_int
at ./src/main.rs:21:5
9: my_crate::stack2
at ./src/main.rs:16:5
10: my_crate::stack1
at ./src/main.rs:11:5
11: my_crate::main
at ./src/main.rs:2:36
...
この方法で、MyError
を使ってカスタムエラーに簡単に変更でき、将来のコード変更に備えることができます。また、anyhow
のエラー生成の利便性も維持されています。