Rust/AnyhowのTips
Rust初学者が苦戦するのことの一つにエラー処理があると思います。自分もそうでした。例外のあるプログラミング言語(C++, C#, Java)から来ると、RustのResult、Optionはとっつきにくさ感じますが、使いこなせば超強力な武器になります。
anyhowは2019頃に出てきたライブラリで、筆者の観測範囲[1]では2021年10月時点におけるデファクトのエラー管理crateと言って良いと思う。ちなみにanyhowのコア開発者のDavid Tolnay氏はRust界では著名な開発者で、serde-rsがよく知られています。本記事では、anyhowを使いこなすためのTipsを紹介してみます。
なぜanyhowを使うのか
なぜanyhowを使うべきなのかは他の記事[2][3]によくまとまっているのでここでは詳細に書きませんが、以下のような理由があります。
- 他のエラー型から
anyhow::Error
への変換が容易 - エラーの階層化
- Backtrace
-
anyhow!
,ensure!
,bail!
,ensure!
等の便利マクロ
anyhow::Result
このようなtypeエイリアスが定義されているので、
pub type Result<T, E = Error> = core::result::Result<T, E>;
Resultに二つ型を指定するより
fn foo() -> Result<(), anyhow::Error> {
...
}
このようにanyhow::Result
を使ったほうがスッキリ書けますね。
use anyhow::Result;
fn foo() -> Result<()> {
...
}
anyhow::Result
を使う
main関数の戻りの型にmain関数の戻り値はstd::process::Terminationを実装している必要がありますが、以下のようなブランケット実装があるのでanyhow::Result
を返すことができます。
impl<E: Debug> Termination for Result<(), E>
つまり
fn main() -> anyhow::Result<()> {
Ok(())
}
asyncもOK
#[tokio::main]
async fn main() -> anyhow::Result<()> {
Ok(())
}
anyhow::Result
を使う
テスト関数の戻りの型に以下のように全てのResult型でunwrapするとコードが長くなってしまいます。
#[test]
fn test_foo() {
Foo::new().unwrap().bar().unwrap();
}
この場合戻り値の型をanyhow::Result
にして?
を使うとスッキリかけます。
#[test]
fn test_foo() -> anyhow::Result<()> {
Foo::new()?.bar()?;
Ok(())
}
Backtrace
Backtraceを表示させるには、nightlyツールチェインを使うかstableで以下のようにfeatureを指定する必要があります。
anyhow = { version = "1", features = ["backtrace"] }
またプログラムを実行するときは環境変数RUST_BACKTRACE=1
をつける必要があります。
{}
, {:?}
, {:#}
, {:#?}
の違い
anyhow::Error
を{}
と{:?}
で文字列出力したときの結果が異なります。
-
{}
の場合
エラーの文字列のみが出力されます。このようなプログラムを実行するとfoo error
が出力されます。
use anyhow::{Context, Result};
fn foo() -> Result<()> {
bar().context("foo error")?;
Ok(())
}
fn bar() -> Result<()> {
baz().context("bar error")?;
Ok(())
}
fn baz() -> Result<()> {
Err(anyhow::anyhow!("baz error"))
}
fn main() {
if let Err(e) = foo() {
println!("{}", e);
}
}
-
{:?}
の場合
エラーの文字列、スタックされたエラーの文字列が出力されます。上記の例で{}
の代わりに{:?}
を使うと以下が出力されます。
foo error
Caused by:
0: bar error
1: baz error
また、前述したbacktrace機能が使える状態になっており、RUST_BACKTRACE=1
の環境変数を設定している場合は、上記のエラー情報にプラスしてbacktraceが出力されます。
foo error
Caused by:
0: bar error
1: baz error
Stack backtrace:
0: std::backtrace_rs::backtrace::libunwind::trace
at /rustc/1f5bc176b0e54a8e464704adcd7e571700207fe9/library/std/src/../../backtrace/src/backtrace/libunwind.rs:90:5
std::backtrace_rs::backtrace::trace_unsynchronized
at /rustc/1f5bc176b0e54a8e464704adcd7e571700207fe9/library/std/src/../../backtrace/src/backtrace/mod.rs:66:5
std::backtrace::Backtrace::create
at /rustc/1f5bc176b0e54a8e464704adcd7e571700207fe9/library/std/src/backtrace.rs:316:13
1: anyhow::error::<impl anyhow::Error>::msg
at /Users/yukinari/.cargo/registry/src/github.com-1ecc6299db9ec823/anyhow-1.0.45/src/error.rs:79:36
2: anyy::baz
at ./src/main.rs:14:9
3: anyy::bar
at ./src/main.rs:9:5
4: anyy::foo
at ./src/main.rs:4:5
5: anyy::main
at ./src/main.rs:18:21
...
-
{:#}
の場合
エラーの文字列、スタックされたエラーが一行で出力されます。サーバープログラム等でエラーをログに残したい場合はこれがおすすめです。
foo error: bar error: baz error
-
{:#?}
の場合
anyhow::Error
がpretty printされます。あまり用途が思いつかないです。
Error {
context: "foo error",
source: Error {
context: "bar error",
source: "baz error",
},
}
.context
を使う
anyhowにはContext
というトレイトがあり、std::error::Error
を実装しているエラー型に新たにエラーを追加しつつanyhow::Error
に変換することができます。
use anyhow::{Context, Result};
fn foo() -> Result<()> {
std::fs::open("config.yml").context("Failed to open config file")?;
...
}
.with_context
を使う
以下のようなformat!
してエラー文を作ったりする場合だと.context
だとエラーじゃなくても毎回format!
が呼ばれてしまいます。with_context
を使えばクロージャを渡せるのでformat!
はエラー時のみ呼ばれるようにできます。
use anyhow::{Context, Result};
fn foo() -> Result<()> {
let path = "config.yml";
std::fs::open(path).with_context(|| format!("Failed to open config file: {}", path))?;
...
}
.context
を使う
Optionに対して意外に知らない人が多いですが、anyhow::Context
はOption<T>に対して実装されているので、.context
、.with_context
が使えます。パラメータのバリデーションするような場面ではとても便利です。
fn validate(param: Param) -> anyhow::Result<()> {
param.foo.context("foo is missing")?;
param.bar.context("bar is missing")?;
}
use anyhow::Context as _
で名前空間汚染を防ぐ
比較的新しいUnderscore Importという機能によって
use anyhow::Context as AnyhowContext;
のようにエイリアスを付けていたのが、_
を使えるようになりました。
use anyhow::Context as _;
詳しくはanyhow::Context を use したいが名前が被ってしまうときの解決策 -> impl-only-useを参照。
anyhow!
を使う
anyhow!
マクロを使えば簡単にanyhow::Error
を作れます。
- エラー型を渡す
use anyhow::{Result, anyhow};
fn foo() -> Result<()> {
std::fs::File::open("a.yml").map_err(|e| anyhow!(e))?;
...
}
- 文字列を渡す
use anyhow::{Result, anyhow};
fn validate(s: &str) -> Result<()> {
if s.len() >= 10 {
return Err(anyhow!("Length must be less than 10"));
}
...
}
-
format!
スタイルを使う
use anyhow::{Result, anyhow};
fn validate(s: &str) -> Result<()> {
if s.len() >= 10 {
return Err(anyhow!("Length of string \"{}\" must be less than 10", s));
}
...
}
筆者の経験上では2か3を使う場合がほとんどでした。1の場合はマクロを使う代わりにanyhow::Error::from
や.context
を使っていました。
bail!
を使う
上記の2, 3のようなケースはbail!
を使うともっと簡単に書けます。
use anyhow::{Result, bail};
fn validate(s: &str) -> Result<()> {
if s.len() >= 10 {
bail!("Length of string \"{}\" must be less than 10", s);
}
...
}
つまりbail!
はreturn Err(anyhow!(...))
の簡略化した書き方です。
ensure!
を使う
上記の例はensure!
を使うとさらに短く書けます。
use anyhow::{Result, ensure};
fn validate(s: &str) -> Result<()> {
ensure!(s.len() < 10, "Length of string \"{}\" must be less than 10", s);
...
}
Discussion