この記事では、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には、それぞれバックトレースを有効にするための機能フラグ(backtrace
とbacktraces
)が設定されています。
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の短所
- 使い方がやや複雑
どちらの方法を選ぶかは、プロジェクトやチームのニーズに応じて決定することが重要です。それぞれの方法には利点と欠点があるため、要件や開発者の好みに応じて最適な方法を選択してください。