Rust: anyhowとsnafuを使って自前エラーにバックトレースをつける方法

この記事では、Rustで自作のエラー型にバックトレースを追加する方法を、anyhowとsnafuを使って説明します。それぞれの方法について、サンプルコードを使って詳しく解説し、長所と短所も述べます。この記事で扱ったサンプルコードの全体は[GitHub](https://github.com/suinplayground/rust/blob/main/how-to-add-backtrace-to-custom-errors/src/main.rs)で見れます。

## サンプルコードの概要

サンプルコードは以下の構成になっています。

- Cargo.toml: 依存関係の設定
- [src/main.rs](https://github.com/suinplayground/rust/blob/main/how-to-add-backtrace-to-custom-errors/src/main.rs): メイン関数と、anyhowとsnafuを使った2つの例

### Cargo.toml

まず、以下のようなCargo.tomlファイルを用意しました。

```toml
[package]
name = "how-to-add-backtrace-to-custom-errors"
version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = { version = "1.0.70", features = ["backtrace"] }
snafu = { version = "0.7.4", features = ["backtraces"] }
thiserror = "1.0.40"
```

ここでは、anyhow、snafu、thiserrorの3つのクレートを依存関係に追加しています。anyhowとsnafuには、それぞれバックトレースを有効にするための機能フラグ（`backtrace`と`backtraces`）が設定されています。

### anyhowを使った例

次に、anyhowを使って自作エラーにバックトレースを追加する例です。

```rust
use anyhow::{ensure, Result};
use thiserror::Error;

fn main() {
    let err = is_valid_id(1).err().unwrap();
    // `{:?}` はエラーとそのバックトレースを表示します。バックトレースを表示するには
    // `RUST_BACKTRACE=1` で実行します。
    // 例: `RUST_BACKTRACE=1 cargo run`
    println!("{:?}", err);

    // `downcast_ref` を使って元のエラー構造体を取得し、各エラーケースを処理できます。
    // ただし、この方法は少しわかりにくいです。なぜなら、関数のシグネチャからエラーの実際の型がわからないからです。
    match err.downcast_ref::<CustomError>() {
        Some(CustomError::MustBeLessThanTen(id)) => {
            println!("You gave me an ID that was too small: {}", id);
        }
        None => {
            println!("Unknown error");
        }
    }
}

fn is_valid_id(id: u16) -> Result<()> {
    // `ensure!` は、条件が偽の場合に与えられたエラーで `Err` を返すマクロです。
    ensure!(id >= 10, CustomError::MustBeLessThanTen(id));
    Ok(())
}

#[derive(Error, Debug)]
enum CustomError {
    #[error("ID may not be less than 10, but it was {0}")]
    MustBeLessThanTen(u16),
}
```

このmainを`RUST_BACKTRACE=1 cargo run`で実行するとスタックトレースは次のように出力されます。

```
ID may not be less than 10, but it was 1

Stack backtrace:
   0: backtrace::backtrace::libunwind::trace
             at /Users/suin/.cargo/registry/src/github.com-1ecc6299db9ec823/backtrace-0.3.67/src/backtrace/libunwind.rs:93:5
... 中略 ...
   6: anyhow::kind::Trait::new
             at /Users/suin/.cargo/registry/src/github.com-1ecc6299db9ec823/anyhow-1.0.70/src/kind.rs:91:9
   7: how_to_add_backtrace_to_custom_errors::with_anyhow::is_valid_id
             at ./src/main.rs:37:9
   8: how_to_add_backtrace_to_custom_errors::with_anyhow::example
             at ./src/main.rs:14:19
   9: how_to_add_backtrace_to_custom_errors::main
             at ./src/main.rs:3:5
  10: core::ops::function::FnOnce::call_once
             at /rustc/2c8cc343237b8f7d5a3c3703e3a87f2eb2c54a74/library/core/src/ops/function.rs:250:5
... 中略 ...
/rustc/2c8cc343237b8f7d5a3c3703e3a87f2eb2c54a74/library/std/src/rt.rs:165:17
  15: _main
You gave me an ID that was too small: 1
```

### snafuを使った例

最後に、snafuを使って自作エラーにバックトレースを追加する例です。

```rust
use snafu::prelude::*;
use snafu::Backtrace;

fn main() {
    let err = is_valid_id(1).err().unwrap();
    // `{:?}` はエラーとそのバックトレースを表示します。
    println!("{:#?}", err);
}

fn is_valid_id(id: u16) -> Result<(), CustomError> {
    // `ensure!` は、条件が偽の場合に与えられたエラーで `Err` を返すマクロです。
    // ここでは、オリジナルの構造体 `MustBeLessThanTen` ではなく、
    // `Snafu` 接尾辞付きの構造体を使用する必要があります。
    ensure!(id >= 10, MustBeLessThanTenSnafu { id });
    Ok(())
}

#[derive(Debug, Snafu)]
enum CustomError {
    #[snafu(display("ID may not be less than 10, but it was {id}"))]
    MustBeLessThanTen { id: u16, backtrace: Backtrace },
}
```

このmainを`cargo run`で実行するとスタックトレースは次のように出力されます。

```
MustBeLessThanTen {
    id: 1,
    backtrace: Backtrace(
           0:        0x102f4c03c - backtrace::backtrace::libunwind::trace::h3f3c4b1490d279fe
                                       at /Users/suin/.cargo/registry/src/github.com-1ecc6299db9ec823/backtrace-0.3.67/src/backtrace/libunwind.rs:93:5
...中略...
           4:        0x102efdcec - <snafu::backtrace_shim::Backtrace as snafu::GenerateImplicitData>::generate::h32eb7fa4770aa3fd
                                       at /Users/suin/.cargo/registry/src/github.com-1ecc6299db9ec823/snafu-0.7.4/src/backtrace_shim.rs:15:19
                                   how_to_add_backtrace_to_custom_errors::with_snafu::MustBeLessThanTenSnafu<__T0>::build::h82c97a554538fbdc
                                       at /Volumes/x/playground/rust/how-to-add-backtrace-to-custom-errors/src/main.rs:67:21
           5:        0x102efdda8 - how_to_add_backtrace_to_custom_errors::with_snafu::MustBeLessThanTenSnafu<__T0>::fail::hd9768315b665eb54
                                       at /Volumes/x/playground/rust/how-to-add-backtrace-to-custom-errors/src/main.rs:67:21
           6:        0x102efdf7c - how_to_add_backtrace_to_custom_errors::with_snafu::is_valid_id::h8a1f8691e13b3219
                                       at /Volumes/x/playground/rust/how-to-add-backtrace-to-custom-errors/src/main.rs:63:9
           7:        0x102efde54 - how_to_add_backtrace_to_custom_errors::with_snafu::example::hd391254d58a32764
                                       at /Volumes/x/playground/rust/how-to-add-backtrace-to-custom-errors/src/main.rs:53:19
           8:        0x102efd4bc - how_to_add_backtrace_to_custom_errors::main::hbc1f31a80106d54a
                                       at /Volumes/x/playground/rust/how-to-add-backtrace-to-custom-errors/src/main.rs:5:5
           9:        0x102efda20 - core::ops::function::FnOnce::call_once::h13e91b59e3f52d45
                                       at /rustc/2c8cc343237b8f7d5a3c3703e3a87f2eb2c54a74/library/core/src/ops/function.rs:250:5
...中略...
          14:        0x102efd4ec - _main
        ,
    ),
}
```

## anyhowとsnafuの長所と短所

それでは、anyhowとsnafuの長所と短所を見ていきましょう。

### anyhowの長所

- thiserrorと相性が良い

### anyhowの短所

- エラーの具体的な型が関数のシグネチャからわからない

### snafuの長所

- エラー型が明確で、関数のシグネチャからわかる

### snafuの短所

- 使い方がやや複雑

どちらの方法を選ぶかは、プロジェクトやチームのニーズに応じて決定することが重要です。それぞれの方法には利点と欠点があるため、要件や開発者の好みに応じて最適な方法を選択してください。