LoginSignup
5
1

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-04-18

この記事では、Rustで自作のエラー型にバックトレースを追加する方法を、anyhowとsnafuを使って説明します。それぞれの方法について、サンプルコードを使って詳しく解説し、長所と短所も述べます。この記事で扱ったサンプルコードの全体はGitHubで見れます。

サンプルコードの概要

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

  • Cargo.toml: 依存関係の設定
  • src/main.rs: メイン関数と、anyhowとsnafuを使った2つの例

Cargo.toml

まず、以下のようなCargo.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には、それぞれバックトレースを有効にするための機能フラグ(backtracebacktraces)が設定されています。

anyhowを使った例

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

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を使って自作エラーにバックトレースを追加する例です。

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の短所

  • 使い方がやや複雑

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

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1