ALT; mermaidによるフローチャート
本記事は hooqアドベントカレンダー 25日目の記事です!メリークリスマス!
hooqはメソッドを ? 演算子にフックする属性マクロです。本アドカレではhooqの使い方を始め、hooqマクロを作成するにあたり得た知識、Rustのエラーハンドリング・エラーロギング周りの話をまとめています。
さて、hooqアドベントカレンダーではhooqの話題に限らずRustのエラー関連クレート全般に関しても扱ってきました!しかしながら筆者にはいまだに引っかかっていることがあります...
結局Rustにおけるエラーハンドリング・ロギングのベストプラクティスは何なのか?
筆者がhooq属性マクロを作ったきっかけの一つは、仕事でよくわからないままtracingクレートを利用する中、 ? 演算子をたくさん含んだ関数が出てきて 正確なエラー発生箇所がわからない !という事件が起きたことでした。
こちらは一応見事解決しました1!
しかしあくまでもきっかけの一つにすぎず、作る決意をした本当の理由は、筆者には普段の書き捨てコードにてエラートレース欲しさにanyhow::Context::with_contextをすべての ? に挿入する悪癖があったためです。その内容も記事にしました。
つまり、筆者はRustの特にエラートレースの取得方法に対して適切な感覚を持っていなかったのです。 だからhooqを作ってしまいました 。
- 「バックトレース(
std::backtrace::Backtrace)を見ればよい/使えばよいのでは?」 - 「tracingクレートの
#[tracing::instrument]を普段使いすればよいのでは?」
hooqの開発中や開発終盤でXにて言われたことの意訳です。「 あんな見辛いバックトレースを皆使ってるのか...?! 」「tracingはさすがに書き捨てコードには合わなくないか...?」などなど、思うところは多かったのですが、「 使ってない技術への文句はなるべく言わない 」が筆者のモットー2なので、良い機会だろうと今回のhooqアドカレにてこれらについて改めて触ってきました!
...色々触ってきた今だからこそ声を大にして言いたいです。
「エラートレースに限らず、 Rustのランタイムエラー周りの "答え" 、落ちてなくないですか...?!?! 」
局所解的にanyhowやthiserrorやその他もろもろの使い方解説はそれはもうたくさん落ちています。しかし、いざ使おうとなった際の 全体を俯瞰した技術選定ノウハウは見かけません 。局所解に埋もれてるのかもしれないですが...
- エラーハンドリング
- いつ
unwrapを使い、いつResultを使うか -
ResultでハンドリングするとしてResult<T, E>のEをどうするか- 全部
anyhow::Errorで良い?本気?
- 全部
- いつ
- エラーロギング
- tracingを使っておけば問題ないのか?
-
?演算子の位置すらわからないくせに? - バックトレースと組み合わせるの?
-
- tracingを使っておけば問題ないのか?
- エラートレース
- tracingがあるならバックトレース要らなくないですか?
- バックトレースがあるならtracing要らなくないですか?
- 結局どっちを使うべきなの?
というわけで、色々触ってきた筆者なりに今回整理してみました!参考にでも叩き台にでもなんでも好きなように調理していただければ幸いです。
ランタイムエラーハンドリング周りに求めるもの整理
具体的にどうするかの話に入る前に、独断と偏見で Result<T, E> の役回りを整理します。
選定フローチャートを拵えるために、選定理由部分を先に揃えておく狙いです。
-
[回復性] -
[網羅性] -
[可視性] -
[追跡性]
Result<T, E> の恩恵
「なぜ皆パニックさせず Result 型を使うのか?」に、求めるものが詰まっています。
[回復性] エラーからの回復をすべてRustの通常の処理系で行える
言わずもがなです。パニック系は基本3回復しませんが、 Result 型はエラーを回復したい時に使うことができ、他の言語と違って「例外処理機構(try-catch)」みたいな例外的な仕組み(例外だけに...)に頼らず、すべて普通のRustコードでエラーをハンドルすることができます。
これらは具体的には次のようなシーンで嬉しいでしょう。
- 常駐アプリケーション(Webサーバー等)でログを出しつつ引き続き新しい処理を行いたい
- 配列から配列の変換で失敗した一部だけログを出した上で無視したい
逆に次のようなシーンではこの性質自体はあまり求められません。
-
fn main() -> Result<(), E> {}でエラーをそのままプログラムの処理結果とする場合-
mainがエラーを返すならパニックと大差ない
-
- ブートストラップ中などで失敗時即終了で問題ない処理
- 良くあるのは環境変数設定忘れなど
以降このメリットを「回復性」と表記します。
[網羅性] E を列挙型にすると match 式でのハンドリング時に網羅性を確認しやすい
前節の回復に関連して、回復する場合 E はトレイトオブジェクト (Box<dyn std::error::Error> など) ではなく通常の列挙型であると match 式でハンドルしやすく可読性向上に寄与します。
アプリクレートでもthiserrorクレートで自前でエラー型を用意したくなるのはこの場合と思います。とはいえやりすぎると型の数が増えすぎて管理が大変になるため「どこまでやるか」そして「どのように定義するか」の議論は尽きなさそうです。
求められるシーン/求められないシーンは前節と同様でしょう。
以降このメリットを「網羅性」と表記します。
[可視性] 返り値型 / ? を通して失敗可能性を示せる
「 関数のシグネチャを見ただけで失敗可能性を内包しているかがわかる 」というのは、 unwrap では絶対に得られない Result 型最大の恩恵と言っても差し支えないかもしれません。これはどのようなシーンでも欲しくなる性質と言えるでしょう。
また呼び出し側からも ? により失敗可能性がわかりますが、これは .unwrap() でもわからなくはないのでRust言語自体が元から備えている良さと言えそうです。
以降このメリットを「可視性」と表記します。
パニック・ Result<T, E> を問わず、エラーに求められること
Result 型であるかどうかにかかわらずランタイムエラーに求められることを記します。
[追跡性] 原因究明のためにトレースを得られる
何かランタイムエラーが発生した際の原因特定のために「どこでエラーが発生してどのようにエラーが伝搬したか」は重要な情報です。
以降この要求を(トレーサビリティ等の方が自然かもしれませんが、他の語に合わせ和訳した)「追跡性」と表記します。
ランタイムエラー技術選定フローチャート
というわけでお待たせしました。ここまで挙げた基準を踏まえてまとめてみた技術選定フローチャートが以下になります!ご査収ください。
ALT; mermaidによるフローチャート
このフローチャートに関して要所要所を簡単に解説していきます!
設定読み込み・ブートストラップ中なら .expect("...") がオススメ
「 .unwrap() 系統によるパニックも適切な場面なら利用するべき」とはよく言いますが、じゃあその適切な場面っていつさ?という話です。筆者は大体は以下2つになると認識しています。
- テストコード
- パニック箇所を知りたいので
.unwrap()が一番便利
- パニック箇所を知りたいので
- 常駐アプリ・Webサーバー等のブートストラップ
-
.expect("...")が最も輝く - 回復させる必要性がない & すぐ気づけるため、
Resultにする必要性は薄い - 「アプリを動かす絶対条件」である(失敗を前提としてない)ことが読めるため可読性的にもこちらの方が良い
-
ブートストラップの方では、もちろん多用しないほうが良さそうではあります。設定読み込みが結構大規模な場合は、設定処理の可視性( ? 演算子位置の把握)のために一番上の呼び出しだけ expect にするといった方針にした方が治安が良さそうです。
回復不要で fn main() -> Result<(), E> {...} なら「color-eyre」か「anyhow + hooq」が良さげ
エラーがあるなら停止してよい類のアプリケーションの場合でも、エラーが発生する箇所を可視化するために Result と ? を使った方が良いでしょう。
エラーの中身で分岐することはないため、 Result<T, E> の E についてはすべてのエラー型を統合できるトレイトオブジェクト Box<dyn std::error::Error> やその仲間の anyhow::Error にするとエラー変換について考えなくても良くなり便利です。
しかしここから罠があり、例えば anyhow::Error では unwrap では得られていたエラー発生箇所情報が、リリースビルド等では見れなくなる という問題があります。
fn func() {
let _ = std::fs::read_to_string("not_found.txt").unwrap();
}
fn main() {
func();
}
thread 'main' (38) panicked at src/main.rs:2:54:
called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }
fn func() -> anyhow::Result<()> {
let _ = std::fs::read_to_string("not_found.txt")?;
Ok(())
}
fn main() -> anyhow::Result<()> {
func()?;
Ok(())
}
Error: No such file or directory (os error 2)
デバッグビルドの場合は RUST_BACKTRACE=1 と共に実行すればバックトレースを見れますが、バックトレースを出力しない場合はこのように発生箇所が不明になってしまいます。ソースコード中のエラー発生箇所明記と引き換えに、実行時の追跡性がおろそかになってしまうわけです。これはいただけません!
eyre / color-eyre ならリリースビルドでも発生箇所がわかる
実はeyre・color-eyreを使っている場合この問題を回避することができます。
fn func() -> eyre::Result<()> {
let _ = std::fs::read_to_string("not_found.txt")?;
Ok(())
}
fn main() -> eyre::Result<()> {
func()?;
Ok(())
}
$ cargo run --release -q
Error: No such file or directory (os error 2)
Location:
src/main.rs:2:13
というわけで、思考停止的に選ぶならanyhowよりはeyreまたはcolor-eyreがまだ良さそうです!
バックトレースを得たいならcolor-eyreがオススメ
しかしエラー発生箇所が分かっただけではエラー原因自体がつかめられず、そのような時はバックトレースがほしいです。
color-eyreを使えば RUST_LIB_BACKTRACE=full を付けることでanyhowよりも綺麗なバックトレースが得られます。通常はデバッグビルドでのみ得られるものですが、リリース最適化をしたい場合でも、Cargo.tomlにて debug = true を仕込んだプロファイルを作成すればバックトレースを得ることが可能です!
[package]
name = "color_eyre_pg"
version = "0.1.0"
edition = "2024"
[dependencies]
color-eyre = "0.6.5"
[profile.release-with-debug]
inherits = "release"
debug = true
use color_eyre::eyre;
fn func() -> eyre::Result<()> {
let _ = std::fs::read_to_string("not_found.txt")?;
Ok(())
}
fn main() -> eyre::Result<()> {
color_eyre::install()?; // 忘れないように!!
func()?;
Ok(())
}
$ RUST_LIB_BACKTRACE=full cargo run -q --profile=release-with-debug
Error:
0: No such file or directory (os error 2)
Location:
src/main.rs:4
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ BACKTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⋮ 5 frames hidden ⋮
6: <core::result::Result<T,F> as core::ops::try_trait::FromResidual<core::result::Result<core::convert::Infallible,E>>>::from_residual::ha86bc67fba6d53b0
at /home/namn/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:2177
2175 │ fn from_residual(residual: Result<convert::Infallible, E>) -> Self {
2176 │ match residual {
2177 > Err(e) => Err(From::from(e)),
2178 │ }
2179 │ }
7: color_eyre_pg::func::h3ec611a2c8b0473a
at /home/namn/workspace/qiita_adv_articles_2025/programs/adv25/color_eyre_pg/src/main.rs:4
2 │
3 │ fn func() -> eyre::Result<()> {
4 > let _ = std::fs::read_to_string("not_found.txt")?;
5 │
6 │ Ok(())
8: color_eyre_pg::main::h48e23c3117003fbb
at /home/namn/workspace/qiita_adv_articles_2025/programs/adv25/color_eyre_pg/src/main.rs:12
10 │ color_eyre::install()?;
11 │
12 > func()?;
13 │
14 │ Ok(())
9: core::ops::function::FnOnce::call_once::hbf09f2fcc9dd975a
at /home/namn/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ops/function.rs:250
248 │ /// Performs the call operation.
249 │ #[unstable(feature = "fn_traits", issue = "29625")]
250 > extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
251 │ }
252 │
10: std::sys::backtrace::__rust_begin_short_backtrace::hd875e56de900772a
at /home/namn/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/sys/backtrace.rs:158
156 │ F: FnOnce() -> T,
157 │ {
158 > let result = f();
159 │
160 │ // prevent this frame from being tail-call optimised away
⋮ 13 frames hidden ⋮
Run with COLORBT_SHOW_HIDDEN=1 environment variable to disable frame filtering.
バックトレースをメイン使いする場合はcolor-eyreがベストな選択でしょう!
anyhow + hooq でエラートレースもどきを得る方法
前節でcolor-eyreを使い色々設定すればバックトレースが得られるという話をしました。毎回この設定を思い出すのはなかなか大変なのではないでしょうか...?その割に多少見やすくなったというだけで相変わらずバックトレースは情報量が多いです。
別な手法として、関数の頭に付けておくだけで簡潔なエラートレースもどきを吐いてくれるhooqマクロをぜひここで紹介させてください!
[package]
name = "anyhow_hooq_pg"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1.0.100"
hooq = "0.3.1"
use hooq::hooq;
#[hooq(anyhow)]
fn func() -> anyhow::Result<()> {
let _ = std::fs::read_to_string("not_found.txt")?;
Ok(())
}
#[hooq(anyhow)]
fn main() -> anyhow::Result<()> {
func()?;
Ok(())
}
$ cargo run -q
Error: [src/main.rs:12:11]
12> func()?
|
Caused by:
0: [src/main.rs:5:53]
5> std::fs::read_to_string("not_...txt")?
|
1: No such file or directory (os error 2)
hooqのからくりは以下の記事で説明しています。
ちなみにフレーバーは各種用意しておりhooqはanyhowではなくeyreやcolor-eyreとも連携可能です。バックトレース以外でエラートレースもどきを得たい場合は有力な候補だと思います!
thiserror並みにエラー型を定義したいし常にエラートレースを得たい → snafuが良さそうかも...?
ここからは中規模以上のアプリケーションで、エラーからの回復を前提としたエラーハンドリング・ロギングをしたい場合の手段について解説していきます。
- thiserrorのように列挙体エラー型をイイ感じに定義したい
- ついでにエラー型にバックトレースを持たせたい
- バックトレースでなくともエラー位置情報を持たせたい
- ついでにanyhowみたいにエラーを一つの型にまとめたい
このようなワガママをすべて叶えてくれそうなクレートにsnafuがあります!
もし筆者がsnafuを使いこなせていたら、おそらく8カ月もかけてhooqを作るなんていう暴挙には出ていなかったかも...
ただ、上記記事にてお試しで使ってみた感じ、snafuは学習コストがそれなりにかかりそうな印象でした。
返り値型や扱い方もsnafuの決まりを踏襲しなければならない感じで、筆者の中では今のところ「色々できる代わりに導入にはそれなりの覚悟が必要そうなクレート」、という位置づけです。
やっぱりthiserror + tracingが安牌
エラー回復を容易に行いたい・エラーハンドリングの網羅性を担保したい場合、やはりthiserrorによる列挙体エラー型定義が一番単純明快で扱いやすいでしょう。
時に、thiserrorでエラー型定義を行うと、stable Rustではstd::backtrace::Backtraceの運搬が困難です。
よって[追跡性]部分が欠損してしまうのですが、 それを補う好都合なクレートが我らがtracingです!
tracingの名前通り詳細なトレースを得るのにこれ以上最適なクレートはないでしょう。エラートレース周りは#[tracing::instrument(err)]ですべて解決です。
以下、thiserrorとtracing両方を使った例です。
use anyhow::anyhow;
use tracing_subscriber::{filter::EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
#[derive(thiserror::Error, Debug)]
enum AppError {
#[error(transparent)]
Reqwest(#[from] reqwest::Error),
#[error(transparent)]
Sqlx(#[from] sqlx::Error),
#[error("Other error occurred: {0}")]
Other(#[from] anyhow::Error),
}
#[tracing::instrument(err)]
fn func1() -> Result<(), AppError> {
Err(anyhow!("An error occurred in func1"))?;
Ok(())
}
#[tracing::instrument(err)]
fn func2() -> Result<(), AppError> {
func1()?;
Ok(())
}
#[tracing::instrument(err)]
fn func3() -> Result<(), AppError> {
func2()?;
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// [追跡性]
subscriber_init()?;
tracing::info!("Starting application");
let res = func3();
// [網羅]的に[回復]
match res {
Ok(_) => {}
Err(AppError::Sqlx(e)) => {
tracing::error!("Database error occurred: {:?}", e);
}
Err(AppError::Reqwest(e)) => {
tracing::error!("HTTP request error occurred: {:?}", e);
}
Err(e) => {
tracing::error!("An error occurred: {:?}", e);
}
}
Ok(())
}
fn subscriber_init() -> Result<(), Box<dyn std::error::Error>> {
let env_filter = EnvFilter::try_from_default_env().or_else(|_| EnvFilter::try_new("debug"))?;
let subscriber = tracing_subscriber::fmt::layer()
.with_file(true)
.with_line_number(true);
tracing_subscriber::registry()
.with(subscriber)
.with(env_filter)
.try_init()?;
Ok(())
}
Cargo.toml
[package]
name = "thiserror_tracing_pg"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1.0.100"
reqwest = "0.12.28"
sqlx = "0.8.6"
thiserror = "2.0.17"
tracing = "0.1.44"
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
tracingを適切に設定していればバックトレースに劣らないトレースが得られます!
$ cargo run -q
2025-12-25T12:28:23.616678Z INFO thiserror_tracing_pg: src/main.rs:39: Starting application
2025-12-25T12:28:23.616801Z ERROR func3:func2:func1: thiserror_tracing_pg: src/main.rs:14: error=Other error occurred: An error occurred in func1
2025-12-25T12:28:23.616861Z ERROR func3:func2: thiserror_tracing_pg: src/main.rs:21: error=Other error occurred: An error occurred in func1
2025-12-25T12:28:23.616926Z ERROR func3: thiserror_tracing_pg: src/main.rs:28: error=Other error occurred: An error occurred in func1
2025-12-25T12:28:23.616975Z ERROR thiserror_tracing_pg: src/main.rs:53: An error occurred: Other(An error occurred in func1)
詳細は以下の記事を読んでみてください。まぁ私の記事以外にも情報は豊富に落ちていると思います。
thiserrorとバックトレース
tracingがある以上必要性を感じにくいですが、nightlyでは thiserrorにより定義した列挙体エラー型も手軽にバックトレースを持てる ようにするための仕組みが整えられています。
これらが安定化したらまたベストプラクティスが変わる日も来るのでしょうか...?
隠し味にhooqはいかが...?
先ほど「 #[tracing::instrument(err)] で全部解決」と言いましたが、冒頭で紹介した通り実は instrument マクロには「自分でイベントを挟まない限り、 エラーが発生した ? 演算子の位置を正確に得ることができない」という欠点があります!
もし正確な ? 演算子の位置が知りたいという人が居ましたら、ぜひhooqマクロを試してみてください!詳細は以下の記事で解説しています。
まとめ・所感
というわけで、筆者なりにエラーハンドリング・ロギングの技術選定フローチャートを描いてみましたが...
正直!まだベストプラクティスがなんなのかモヤモヤしています!!!!
結局アプリクレートでもthiserrorで詳細にエラー型を定義してよいのか、アンチパターンではなかろうか、とか、「 #[tracing::instrument] で雑にまとめる」としたけど何かもっと気にすることがあるのではなかろうか、とか。全然まだよくわかってません。
本記事ではあくまでも筆者の現時点での考え方を記しただけです。「ご高説どうもありがとうございます。お前のフローチャートは全然ダメダメです。私が本物のRustエラーハンドリングベストプラクティスを教えてやりますよ!!」という人がもしいましたら、 ぜひマサカリをお願いします 。ドキュメントも議論も足りないんじゃ...
以上、hooqアドベントカレンダー25日目の記事でした!本アドベントカレンダーが、読者の方がRustのエラーハンドリング・ロギングを再考されるきっかけになっていたら幸いです。
ここまで読んでいただきありがとうございました!良いお年を~
