❓

中途半端にResultを使うぐらいならunwrapの方がマシ

に公開

Rust初心者の筆者「アドカレ完走!書いた文字数をカウントしてブログに乾燥した換装書くぞ!」

初筆「せっかくだし文字数カウントスクリプトもRustで書いてみるか。ディレクトリにあるファイル群を読み込んで各ファイルの文字数を表示しよう」

src/main.rs
fn get_files(dir_name: &str) -> Vec<String> {
    let mut files = Vec::new();

    let entries = std::fs::read_dir(dir_name).unwrap();

    for entry in entries {
        let entry = entry.unwrap();
        let path = entry.path();
        if path.is_file() {
            files.push(path.to_string_lossy().into_owned());
        }
    }

    files
}

fn chars_count(dir_name: &str, file_name: &str) -> usize {
    let content = std::fs::read_to_string(format!("{}/{}", dir_name, file_name)).unwrap();
    content.chars().count()
}

fn main() {
    let dir_name = std::env::args().nth(1).unwrap();

    let files = get_files(&dir_name);

    println!("Files in directory '{}':", dir_name);

    let mut sum = 0;
    for file in files {
        let count = chars_count(&dir_name, &file);
        sum += count;
        println!("- {file}: {count} characters");
    }

    println!("Total characters: {}", sum);
}

初筆「 Result はまだよくわかんないし全部 unwrap にしちゃった...まだまだ勉強しなきゃいけないこといっぱいある」

初筆「...」

初筆「まぁいいや、実行してみよ」

$ cargo run -q -- articles
Files in directory 'articles':

thread 'main' (56549) panicked at src/main.rs:18:82:
called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

初筆「アレ?!エラーだ!... src/main.rs:18:82 と書いてある から18行目... chars_count 関数に原因がありそう! println! をこの上に挟んで...」


...ジジジ...タイムリープ発生[1]!

2025/12/25に戻った筆者であった...

n回目のループの後...


Rust中級者になった筆者「アドカレ完走!書いた文字数をカウントしてブログに間奏した観想書くぞ!」

中筆「せっかくだし文字数カウントスクリプトもRustで書いてみるか。ディレクトリにあるファイル群を読み込んで各ファイルの文字数を表示しよう」

src/main.rs
use std::path::{Path, PathBuf};

use clap::Parser;

#[derive(Debug, Parser)]
struct Cli {
    /// ディレクトリパス
    dir_name: PathBuf,
}

fn chars_count(dir: &Path, file: &Path) -> anyhow::Result<usize> {
    let content = std::fs::read_to_string(dir.join(file))?;
    Ok(content.chars().count())
}

fn main() -> anyhow::Result<()> {
    let Cli { dir_name } = Cli::parse();

    println!("Files in directory '{}':", dir_name.to_string_lossy());

    let mut sum = 0;
    for entry in std::fs::read_dir(&dir_name)? {
        let file_path = entry?.path();
        let count = chars_count(&dir_name, &file_path)?;
        println!("- {}: {} characters", file_path.to_string_lossy(), count);
        sum += count;
    }

    println!("Total characters: {}", sum);

    Ok(())
}

中筆「エラー型は全部 anyhow::Result でおk😎実行するぞ!」

src/main.rs
$ cargo run -q -- articles/
Files in directory 'articles/':
Error: No such file or directory (os error 2)

中筆「アレ?!エラーだ!... ファイルがない? ...そんなバカな!! dbg! をこの上に挟んで...ああもう、 どこでエラーになったんだよぉ! 」


anyhow::Result を使うと、 .unwrap() よりも得られる情報が少なくなってしまいました 。中途半端に Result を使うぐらいなら .unwrap() の方がマシということでしょうか?(タイトル回収)

そんなことはないはずです。しかし皆さん、中級者筆者の何がいけなかったのか答えを用意できますか?

「anyhowではなくeyreを使えば...」「 RUST_BACKTRACE=1 をつけて実行すれば...」この辺りが想定回答ですが、この答えは手を動かして初めて筆者は知りました。

上記はあくまでも一例で、思考停止で雑に Result 型を使った結果本末転倒なことになる例は他にもありそうです。「 Result 型最高!!」とどのRustaceanも口を揃えて言いますが、「 目的意識をもって Result 型を使っていますか? 」ということを問いたいです。

この問に対する筆者の答えとして、本記事では、下記2つの解説をします。

  • Result 型の恩恵/課題点の整理
  • 目的を意識したエラー関連クレートの選定

皆さんの日ごろの Result 型への意識と照らし合わせて、持ち帰ってもらえる知識があれば幸いです。

Result 型の恩恵/課題点の整理

そもそもなぜ Result 型を使うのか、使う場合何に気を配る必要があるかを整理しましょう。

筆者は以下4つの点を押さえておくべきだと考えています。

  • ✅ 回復性
  • ✅ 網羅性
  • ✅ 可視性
  • ⚠️ 追跡性

凡例 ✅: Result 型のメリット、 ⚠️: Result 型の課題点

詳しく解説します。

✅回復性「エラーからの回復をすべてRustの通常の処理系で行える」

Rust
fn main() {
    let str_arr = ["0", "error", "2"];

    let num_vec: Vec<i32> = str_arr
        .iter()
        // エラーになったメンバは除外することで変換失敗エラーから回復
        .filter_map(|s| s.parse::<i32>().ok())
        .collect();

    println!("{num_vec:?}");
}

パニック系は基本[2]回復しませんが、 Result 型はエラーから回復したい時に使うことができ、他の言語と違って「例外処理機構(try-catch)」みたいな例外的な仕組み(例外だけに...)に頼らず、すべて普通のRustコードでエラーをハンドルすることができます。

以降このメリットを「 回復性 」と表記します。

回復性は具体的には次のようなシーンで嬉しいでしょう。

  • 常駐アプリケーション(Webサーバー等)でログを出しつつ引き続き新しい処理を行いたい
  • 配列から配列の変換で失敗した一部だけログを出した上で無視したい

逆に次のようなシーンでは回復性はあまり求められません。

  • fn main() -> Result<(), E> {} [3]でエラーをそのままプログラムの処理結果とする場合
    • main がエラーを返すなら得られる情報はパニックと大差ない
  • ブートストラップ中などで失敗時即終了で問題ない処理
    • よくあるのは環境変数設定忘れなど

✅網羅性「列挙型の E について match 式で網羅性確認可能」

Rust
// 起きうるエラーを列挙型で設計・確認できる
#[derive(Debug, thiserror::Error)]
enum AppError {
    #[error("Validation Error: {0}")]
    ValidationError(#[from] serde_json::Error),

    #[error("Server Error: {0}")]
    ServerError(#[from] anyhow::Error),
}

impl IntoResponse for AppError {
    fn into_response(self) -> axum::response::Response {
        // 起きうるエラーの考慮漏れがないことをmatch式で保証
        match &self {
            AppError::ValidationError(err) => {
                (axum::http::StatusCode::BAD_REQUEST, err.to_string())
            }
            AppError::ServerError(err) => (
                axum::http::StatusCode::INTERNAL_SERVER_ERROR,
                err.to_string(),
            ),
        }
        .into_response()
    }
}

前節の回復に関連して、回復する場合 E はトレイトオブジェクト (Box<dyn std::error::Error> など) ではなく通常の列挙型であると match 式でハンドルしやすく可読性向上に寄与します。

アプリクレートでもthiserrorで自前でエラー型を用意したくなるのはこの場合と思います。とはいえやりすぎると型の数が増えすぎて管理が大変になるため「どこまでやるか」そして「どのように定義するか」の議論は尽きなさそうです。

求められるシーン/求められないシーンは回復性と同様でしょう。

以降このメリットを「 網羅性 」と表記します。

✅可視性「返り値型 / ? を通して失敗可能性を示せる」

Rust
// シグネチャだけで失敗可能性を判別可能
fn plan() -> anyhow::Result<()> {
    Ok(())
}
fn r#do() -> anyhow::Result<()> {
    Err(anyhow::anyhow!("delay"))
}
fn check() -> anyhow::Result<()> {
    Err(anyhow::anyhow!("cancel"))
}
fn action() -> anyhow::Result<()> {
    Err(anyhow::anyhow!("apologize"))
}

fn main() -> anyhow::Result<()> {
    // エラーが起きうる箇所が4つであることは `?` を見れば自明
    plan()?;
    r#do()?;
    check()?;
    action()?;
    Ok(())
}

「 関数のシグネチャを見ただけで失敗可能性を内包しているかがわかる 」というのは、 unwrap では絶対に得られない Result 型最大の恩恵と言っても差し支えないかもしれません。これはどのようなシーンでも欲しくなる性質と言えるでしょう。

また呼び出し側からも ? により失敗可能性がわかりますが、これは .unwrap() でもわからなくはないのでRust言語自体が元から備えている良さと言えそうです。

以降このメリットを「 可視性 」と表記します。

⚠️追跡性「原因究明のためにトレースを得たい」

Rust
use hooq::hooq;

#[hooq(anyhow)]
fn func1() -> anyhow::Result<()> {
    Err(anyhow::anyhow!("An error occurred in func1"))
}

#[hooq(anyhow)]
fn func2() -> anyhow::Result<()> {
    func1()?;
    Ok(())
}

#[hooq(anyhow)]
fn func3() -> anyhow::Result<()> {
    func2()?;
    Ok(())
}

#[hooq(anyhow)]
fn main() -> anyhow::Result<()> {
    func3()?;
    Ok(())
}
実行結果
$ cargo run -q
Error: [src/main.rs:22:12]
  22>    func3()?
    |

Caused by:
    0: [src/main.rs:16:12]
         16>    func2()?
           |
    1: [src/main.rs:10:12]
         10>    func1()?
           |
    2: [src/main.rs:5:5]
          5>    Err(anyhow::anyhow!("An error occurred in func1"))
           |
    3: An error occurred in func1

何かランタイムエラーが発生した際の原因特定のために「 どこでエラーが発生してどのようにエラーが伝搬したか 」はとてもとても重要な情報です。

以降この課題点ないし要求を、(トレーサビリティ等の方が自然かもしれませんが、他の語に合わせ和訳した)「 追跡性 」と表記します。

本記事の出発点は追跡性が得られないことでしたし、エラークレート選定の鍵も追跡性が握っています。

しかしながら 素の Result 型に関して言えば追跡性は皆無です。そしてRust言語標準で用意されている仕組みは std::backtrace::Backtrace ぐらいしかなく(anyhowやeyre等で利用されています)、 Backtrace 以外の技術では tracing ぐらいしかありません(でした)。 素の状態での追跡性の乏しさ と 選択肢の少なさ が Result 型の課題といえます。


Result 型のメリット「回復性」「網羅性」「可視性」と課題点「追跡性」の詳細について説明しました。つまるところ筆者は 「 Result 型は『追跡性』をどうやって確保しているの??」 ということに一番関心があるわけです。

「回復性」「網羅性」「可視性」「追跡性」の4つを天秤にかけ、各項目についてどれぐらいのレベルで・どのように確保したいかで選ぶべき手法・エラー関連クレートが変わってくるでしょう。これらを踏まえ、記事の残りでは今の筆者なりのエラークレート選定フローチャートを載せます。

エラー関連クレートの選定

大見得を切りましたがシンプルにしてしまえば2択です。思考停止で決めたとしてもあまり間違いは起きないかと思います。

  • 小規模アプリなら color_eyre
    • バックトレースを使わないなら eyre でおk
  • 大規模アプリならやっぱり tracing で追跡性確保。他は自由

次のフローチャートはより細かく考えたものです。

ALT; mermaidによるフローチャート

記事の残りではこのフローチャートの要所要所を簡単に説明していきます!

設定読み込み・ブートストラップ中なら .expect("...") がオススメ

「 .unwrap() 系統によるパニックも適切な場面なら利用するべき」とはよく言いますが、ではその適切な場面っていつ?という話です。筆者は大体は以下2つになると認識しています。

  • テストコード
    • パニック箇所を知りたいので .unwrap() が一番便利
  • 常駐アプリ・Webサーバー等の ブートストラップ
    • .expect("...") が最も輝く
    • 回復させる必要性がない & すぐ気づけるため、 Result にする必要性は薄い
    • 「アプリを動かす絶対条件」である(失敗を前提としてない)ことが読めるため可読性的にもこちらの方が良い

ブートストラップの方では、もちろん多用しないほうが良さそうではあります。設定読み込みが結構大規模な場合は、設定処理の可視性( ? 演算子位置の把握)のために一番上の呼び出しだけ expect にするといった方針にした方が治安が良さそうです。

回復不要で fn main() -> Result<(), E> {...} なら「color-eyre」か「anyhow系 + hooq」が良さげ

エラーがあるなら停止してよい類のCLIアプリケーション等の場合でも、エラーが発生する箇所を可視化するために Result と ? を使った方が良いでしょう。

そしてこれを実現するために、しばしば main 関数のシグネチャを fn main() -> Result<(), E> {...} (ただし E: Debug ) にすると思います。

main関数の返り値の話

main 関数の返り値はTerminationトレイトを実装している必要があり、Result は E: Debug の時に Termination を実装しているため、 E: Debug であれば Result を main 関数の返り値型にできます。

回復不要なアプリでは、エラーの中身で分岐することはないため、 Result<T, E> の E についてはすべてのエラー型を統合できるトレイトオブジェクト Box<dyn std::error::Error> やその仲間の anyhow::Error にするとエラー変換について考えなくても良くなり便利です。

しかしここから罠があり、冒頭のタイムリープで示した通り例えば anyhow::Error では unwrap では得られていたエラー発生箇所情報が、リリースビルド等では見れなくなる という問題があります。

src/main.rs
fn func() {
    let _ = std::fs::read_to_string("not_found.txt").unwrap();
}

fn main() {
    func();
}
パニックなら2行目でエラーになったとわかる
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" }
src/main.rs
fn func() -> anyhow::Result<()> {
    let _ = std::fs::read_to_string("not_found.txt")?;
    
    Ok(())
}

fn main() -> anyhow::Result<()> {
    func()?;
    
    Ok(())
}
anyhow::Resultだと発生位置不明!
Error: No such file or directory (os error 2)

デバッグビルドの場合は RUST_BACKTRACE=1 と共に実行すればバックトレースを見れますが、バックトレースを出力しない場合はこのように発生箇所が不明になってしまいます。ソースコード中のエラー発生箇所明記と引き換えに、実行時の追跡性がおろそかになってしまうわけです。これはいただけません!

eyre / color-eyre ならリリースビルドでも発生箇所がわかる

実はeyre・color-eyreを使っている場合この問題を回避することができます。詳細は調べていませんが多分std::panic::Location を利用している...?

src/main.rs
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_BACKTRACE=full (または RUST_LIB_BACKTRACE=full ) を付けることでanyhowよりも綺麗なバックトレースが得られます。通常はデバッグビルドでのみ得られるものですが、リリース最適化をしたい場合でも、Cargo.tomlにて debug = true を仕込んだプロファイルを作成すればバックトレースを得ることが可能です!

Cargo.toml
[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
src/main.rs
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マクロをぜひここで紹介させてください!

Cargo.toml
[package]
name = "anyhow_hooq_pg"
version = "0.1.0"
edition = "2024"

[dependencies]
anyhow = "1.0.100"
hooq = "0.3.1"
src/main.rs
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のからくりは以下の記事で説明しています。

https://qiita.com/namn1125/items/5d9bdad72056c3c8e096#解決方法の2-hooqを使う

ちなみにフレーバーは各種用意しておりhooqはanyhowではなくeyreやcolor-eyreとも連携可能です。バックトレース以外でエラートレースもどきを得たい場合は有力な候補だと思います!

回復性・網羅性・追跡性すべて欲しい → snafuが良さそうかも...?

ここからは中規模以上のアプリケーションで、エラーからの回復を前提としたエラーハンドリング・ロギングをしたい場合の手段について解説していきます。

  • thiserrorのように列挙体エラー型をイイ感じに定義したい
  • ついでにエラー型にバックトレースを持たせたい
  • ついでにanyhowみたいにエラーを一つの型にまとめたい

このようなワガママをすべて叶えてくれそうなクレートにsnafuがあります!

https://docs.rs/snafu/latest/snafu/index.html

もし筆者がsnafuを使いこなせていたら、おそらく8カ月もかけてhooqを作るなんていう暴挙には出ていなかったかも...

https://qiita.com/namn1125/items/39f7b09644cafeebeb55

ただ、上記記事にてお試しで使ってみた感じ、snafuは学習コストがそれなりにかかりそうな印象でした。

返り値型や扱い方もsnafuの決まりを踏襲しなければならない感じで、筆者の中では今のところ「色々できる代わりに導入にはそれなりの覚悟が必要そうなクレート」、という位置づけです。

日本語の記事でまともなものは一つも見つけてないので、導入しうまく活用できているという人がいたらぜひノウハウを教えてほしいです。

やっぱりthiserror + tracingが安牌

エラー回復を容易に行いたい・エラーハンドリングの網羅性を担保したい場合、やはりthiserrorによる列挙体エラー型定義が一番単純明快で扱いやすいでしょう。

https://docs.rs/thiserror/latest/thiserror/

時に、thiserrorでエラー型定義を行うと、stable Rustではstd::backtrace::Backtraceの運搬が困難です。

よって[追跡性]部分が欠損してしまうのですが、 それを補う好都合なクレートが我らがtracingです!

https://docs.rs/tracing/latest/tracing/

tracingの名前通り詳細なトレースを得るのにこれ以上最適なクレートはないでしょう。エラートレース周りは#[tracing::instrument(err)]ですべて解決です。

以下、thiserrorとtracing両方を使った例です。

src/main.rs
use axum::response::IntoResponse;
use axum::routing::get;
use axum::{Json, Router};
use tracing_subscriber::EnvFilter;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;

// 起きうるエラーを列挙型で設計・確認できる
#[derive(Debug, thiserror::Error)]
enum AppError {
    #[error("Validation Error: {0}")]
    ValidationError(#[from] serde_json::Error),

    #[error("Server Error: {0}")]
    ServerError(#[from] anyhow::Error),
}

impl IntoResponse for AppError {
    fn into_response(self) -> axum::response::Response {
        // 起きうるエラーの考慮漏れがないことをmatch式で保証
        match &self {
            AppError::ValidationError(err) => {
                (axum::http::StatusCode::BAD_REQUEST, err.to_string())
            }
            AppError::ServerError(err) => (
                axum::http::StatusCode::INTERNAL_SERVER_ERROR,
                err.to_string(),
            ),
        }
        .into_response()
    }
}

#[derive(Debug, serde::Deserialize)]
struct InputData {
    value: i32,
}

// tracing::instrumentマクロで[追跡性]確保
#[tracing::instrument(err)]
async fn calc() -> Result<i32, AppError> {
    let input: InputData = serde_json::from_str("invalid json")?;
    Ok(input.value)
}

// tracing::instrumentマクロで[追跡性]確保
#[tracing::instrument(err)]
async fn func1() -> Result<i32, AppError> {
    let result = calc().await?;
    Ok(result)
}

// tracing::instrumentマクロで[追跡性]確保
#[tracing::instrument(err)]
async fn func2() -> Result<i32, AppError> {
    let result = func1().await?;
    Ok(result)
}

// tracing::instrumentマクロで[追跡性]確保
#[tracing::instrument(err)]
async fn func3() -> Result<i32, AppError> {
    let result = func2().await?;
    Ok(result)
}

// tracing::instrumentマクロで[追跡性]確保
#[tracing::instrument(err)]
async fn handler() -> Result<Json<String>, AppError> {
    let result = func3().await?;

    Ok(Json(format!("Result: {}", result)))
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // [追跡性]のために設定
    subscriber_init()?;

    let app = Router::new().route("/", get(handler));

    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await?;

    tracing::info!("Starting application");

    axum::serve(listener, app).await?;

    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
Cargo.toml
[package]
name = "exhaustive"
version = "0.1.0"
edition = "2024"

[dependencies]
anyhow = "1.0.100"
axum = "0.8.8"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.148"
thiserror = "2.0.17"
tokio = { version = "1.48.0", features = ["full"] }
tracing = "0.1.44"
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }

tracingを適切に設定していればバックトレースに劣らないトレースが得られます!

実行結果
$ cargo run -q
2025-12-28T18:09:36.204626Z  INFO exhaustive: src/main.rs:84: Starting application
2025-12-28T18:09:38.492293Z ERROR handler:func3:func2:func1:calc: exhaustive: src/main.rs:40: error=Validation Error: expected value at line 1 column 1
2025-12-28T18:09:38.492403Z ERROR handler:func3:func2:func1: exhaustive: src/main.rs:47: error=Validation Error: expected value at line 1 column 1
2025-12-28T18:09:38.492475Z ERROR handler:func3:func2: exhaustive: src/main.rs:54: error=Validation Error: expected value at line 1 column 1
2025-12-28T18:09:38.492539Z ERROR handler:func3: exhaustive: src/main.rs:61: error=Validation Error: expected value at line 1 column 1
2025-12-28T18:09:38.492603Z ERROR handler: exhaustive: src/main.rs:68: error=Validation Error: expected value at line 1 column 1

詳細は以下の記事を読んでみてください。まぁ私の記事以外にも情報は豊富に落ちていると思います。

https://qiita.com/namn1125/items/93fc9916785fa85c56b2

thiserrorとバックトレース

tracingがある以上必要性を感じにくいですが、nightlyでは thiserrorにより定義した列挙体エラー型も手軽にバックトレースを持てる ようにするための仕組みが整えられています。

これらが安定化したらまたベストプラクティスが変わる日も来るのでしょうか...?

https://qiita.com/namn1125/items/bb81218d033113ae64af#provideメソッド

https://qiita.com/namn1125/items/f236381021272f00a5c0

隠し味にhooqはいかが...?

先ほど「 #[tracing::instrument(err)] で全部解決」と言いましたが、実は instrument マクロには「自分でイベントを挟まない限り、 エラーが発生した ? 演算子の位置を正確に得ることができない」という欠点があります!

もし正確な ? 演算子の位置が知りたいという人が居ましたら、ぜひhooqマクロを試してみてください!詳細は以下の記事で解説しています。

https://qiita.com/namn1125/items/4e2326e5a616aa439a17

まとめ・所感

「どのように Result 型を使うか? unwrap では得られていた追跡性をどのように確保するのか?」という問いに対して、筆者なりに下記の答えを用意しました。

  • 小規模アプリケーションならcolor-eyreとバックトレースを利用する
  • 大規模アプリケーションならtracingを利用する

そして Result 型のメリットも再考しつつ細かく精査できたと思います。

ただこれが唯一解だとは全く思っていません。 偉そうに解説してきましたが本記事に書いた内容を全部実践してきたというわけではなく、実際にやってみたらもっと考えるべき事柄が出てくる気がしています。

実践的なエラークレート選定方法のノウハウを見かけなかったので今回本記事を投稿しました。もし良い解説サイトがあるなら、あるいは皆さんの中にも一家言あるなら、 ぜひ教えてほしいです。

本記事が Result 型によるエラーハンドリング再考の一助になりましたら幸いです。というわけで、ここまで読んでくださりありがとうございました!

脚注
  1. ところでワルプルギスの廻天楽しみですね! ↩︎

  2. 一応捕捉する機構はあった気がします。今回は触れません。 ↩︎

  3. 例えば fn main() -> Result<(), impl Debug> {} とかの方が条件を細かく書けているかもしれませんが、ここでは読みやすさを優先しています。 ↩︎

Discussion