🦀

Rust/AnyhowのTips

2021/12/01に公開

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<()> {
    ...
}

main関数の戻りの型にanyhow::Resultを使う

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))?;

    ...
}

Optionに対して.contextを使う

意外に知らない人が多いですが、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を作れます。

  1. エラー型を渡す
use anyhow::{Result, anyhow};

fn foo() -> Result<()> {
    std::fs::File::open("a.yml").map_err(|e| anyhow!(e))?;
    ...
}
  1. 文字列を渡す
use anyhow::{Result, anyhow};

fn validate(s: &str) -> Result<()> {
    if s.len() >= 10 {
        return Err(anyhow!("Length must be less than 10"));
    }
    ...
}
  1. 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);
    ...
}
脚注
  1. Rustエラーライブラリのトレンド解説(2020年1月版) ↩︎

  2. Rust エラー処理2020 - 電気ひつじ牧場 ↩︎

  3. rust のエラーライブラリは failure を使わないでください ↩︎

GitHubで編集を提案

Discussion