8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

thiserror vs anyhow、エラーハンドリング宗教戦争

Last updated at Posted at 2025-12-10

はじめに

Rustのエラーハンドリング、標準の Result<T, E> だけだと結構つらい。

そこで登場するのが2大クレート:

  • thiserror : カスタムエラー型を作りやすくする
  • anyhow : エラー処理を楽にする

「どっち使えばいいの?」という宗教戦争が勃発しがち。

この記事で決着をつけます(つかない)。

目次

  1. 標準のエラーハンドリングの問題
  2. thiserrorとは
  3. anyhowとは
  4. 使い分けの基準
  5. 両方使うパターン
  6. 実践的なエラー設計
  7. まとめ

標準のエラーハンドリングの問題

問題1:カスタムエラー型が面倒

use std::fmt;
use std::error::Error;

#[derive(Debug)]
enum MyError {
    IoError(std::io::Error),
    ParseError(std::num::ParseIntError),
    Custom(String),
}

impl fmt::Display for MyError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            MyError::IoError(e) => write!(f, "IO error: {}", e),
            MyError::ParseError(e) => write!(f, "Parse error: {}", e),
            MyError::Custom(msg) => write!(f, "{}", msg),
        }
    }
}

impl Error for MyError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            MyError::IoError(e) => Some(e),
            MyError::ParseError(e) => Some(e),
            MyError::Custom(_) => None,
        }
    }
}

impl From<std::io::Error> for MyError {
    fn from(e: std::io::Error) -> Self {
        MyError::IoError(e)
    }
}

impl From<std::num::ParseIntError> for MyError {
    fn from(e: std::num::ParseIntError) -> Self {
        MyError::ParseError(e)
    }
}

長い。 エラー1種類追加するたびにこれを書くのはつらい。

問題2:エラーの伝播が冗長

fn process() -> Result<(), MyError> {
    let content = std::fs::read_to_string("file.txt")
        .map_err(|e| MyError::IoError(e))?;
    let num: i32 = content.trim().parse()
        .map_err(|e| MyError::ParseError(e))?;
    Ok(())
}

? だけで済ませたい...

thiserrorとは

derive マクロでカスタムエラー型を楽に定義できるクレート。

[dependencies]
thiserror = "1.0"

基本的な使い方

use thiserror::Error;

#[derive(Error, Debug)]
enum MyError {
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
    
    #[error("Parse error: {0}")]
    Parse(#[from] std::num::ParseIntError),
    
    #[error("Custom error: {0}")]
    Custom(String),
}

これだけで:

  • Display トレイトが自動実装
  • Error トレイトが自動実装
  • #[from]From トレイトも自動実装

Before vs After

// Before: 約50行
#[derive(Debug)]
enum MyError { ... }
impl fmt::Display for MyError { ... }
impl Error for MyError { ... }
impl From<std::io::Error> for MyError { ... }
impl From<ParseIntError> for MyError { ... }

// After: 約10行
#[derive(Error, Debug)]
enum MyError {
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
    #[error("Parse error: {0}")]
    Parse(#[from] std::num::ParseIntError),
}

エラーメッセージのカスタマイズ

#[derive(Error, Debug)]
enum ValidationError {
    #[error("名前が空です")]
    EmptyName,
    
    #[error("年齢 {age} は範囲外です (0-150)")]
    InvalidAge { age: i32 },
    
    #[error("ユーザー {id} が見つかりません")]
    UserNotFound { id: u64 },
    
    #[error(transparent)]  // 内部エラーをそのまま表示
    Other(#[from] anyhow::Error),
}

anyhowとは

エラー型を気にせず楽にエラー処理するためのクレート。

[dependencies]
anyhow = "1.0"

基本的な使い方

use anyhow::{Result, Context, anyhow, bail};

fn read_config() -> Result<Config> {
    let content = std::fs::read_to_string("config.toml")
        .context("設定ファイルの読み込みに失敗")?;
    
    let config: Config = toml::from_str(&content)
        .context("設定ファイルのパースに失敗")?;
    
    Ok(config)
}

Result型のエイリアス

// anyhowのResult
type Result<T> = std::result::Result<T, anyhow::Error>;

// 使用例
fn process() -> anyhow::Result<()> {
    // 任意のエラー型を返せる
    std::fs::read_to_string("file.txt")?;
    "not a number".parse::<i32>()?;
    Ok(())
}

便利なマクロ

use anyhow::{anyhow, bail, ensure};

fn validate(age: i32) -> anyhow::Result<()> {
    // bail!: エラーを返して早期リターン
    if age < 0 {
        bail!("年齢は0以上である必要があります");
    }
    
    // ensure!: 条件を満たさなければエラー
    ensure!(age <= 150, "年齢は150以下である必要があります");
    
    // anyhow!: エラー値を作成
    if age == 42 {
        return Err(anyhow!("42は禁止された年齢です"));
    }
    
    Ok(())
}

context でエラーに情報を追加

use anyhow::Context;

fn process_file(path: &str) -> anyhow::Result<()> {
    let content = std::fs::read_to_string(path)
        .with_context(|| format!("ファイル '{}' の読み込みに失敗", path))?;
    
    // エラー時の出力例:
    // Error: ファイル 'config.toml' の読み込みに失敗
    // 
    // Caused by:
    //     No such file or directory (os error 2)
    
    Ok(())
}

使い分けの基準

ライブラリ → thiserror

// my_lib/src/lib.rs
use thiserror::Error;

#[derive(Error, Debug)]
pub enum LibraryError {
    #[error("接続に失敗: {0}")]
    Connection(String),
    #[error("タイムアウト")]
    Timeout,
}

pub fn connect(url: &str) -> Result<Connection, LibraryError> {
    // ...
}

理由:

  • ライブラリの利用者がエラーをパターンマッチしたい
  • 明確なエラー型を公開APIとして提供すべき
  • anyhow::Error を返すと、利用者がエラーの種類を判別できない

アプリケーション → anyhow

// my_app/src/main.rs
use anyhow::{Result, Context};

fn main() -> Result<()> {
    let config = load_config()
        .context("設定の読み込みに失敗")?;
    
    run_server(config)
        .context("サーバーの起動に失敗")?;
    
    Ok(())
}

理由:

  • アプリケーションはエラーを表示して終了するだけのことが多い
  • 異なるライブラリからのエラーを統一的に扱える
  • エラーにコンテキストを追加しやすい

判断フローチャート

このコードは...
    │
    ├─ ライブラリである
    │   └─ 利用者がエラーをパターンマッチする?
    │       ├─ Yes → thiserror
    │       └─ No → thiserror(でも将来のために)
    │
    └─ アプリケーションである
        └─ エラーを細かく分類する必要がある?
            ├─ Yes → thiserror(または両方)
            └─ No → anyhow

両方使うパターン

実際のプロジェクトでは 両方使う ことが多い。

// エラー定義(thiserror)
use thiserror::Error;

#[derive(Error, Debug)]
pub enum AppError {
    #[error("設定エラー: {0}")]
    Config(String),
    
    #[error("データベースエラー")]
    Database(#[from] sqlx::Error),
    
    #[error("その他のエラー")]
    Other(#[from] anyhow::Error),  // anyhowを内部に持つ
}

// アプリケーションコード(anyhow)
use anyhow::{Context, Result};

fn process() -> Result<(), AppError> {
    let data = fetch_data()
        .context("データ取得に失敗")?;  // anyhowのcontext
    
    save_to_db(data)?;  // sqlx::Error → AppError::Database
    
    Ok(())
}

ベストプラクティス

// src/error.rs
use thiserror::Error;

#[derive(Error, Debug)]
pub enum Error {
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
    
    #[error("JSON error: {0}")]
    Json(#[from] serde_json::Error),
    
    #[error("{0}")]
    Custom(String),
}

// カスタムResult型
pub type Result<T> = std::result::Result<T, Error>;

// src/lib.rs
mod error;
pub use error::{Error, Result};

// 使用側
pub fn process() -> Result<Data> {
    let content = std::fs::read_to_string("data.json")?;
    let data: Data = serde_json::from_str(&content)?;
    Ok(data)
}

実践的なエラー設計

パターン1:レイヤーごとにエラー型

// ドメイン層
#[derive(Error, Debug)]
pub enum DomainError {
    #[error("ユーザーが見つかりません: {0}")]
    UserNotFound(u64),
    #[error("権限がありません")]
    Unauthorized,
}

// インフラ層
#[derive(Error, Debug)]
pub enum InfraError {
    #[error("データベースエラー: {0}")]
    Database(#[from] sqlx::Error),
    #[error("キャッシュエラー: {0}")]
    Cache(#[from] redis::RedisError),
}

// アプリケーション層(統合)
#[derive(Error, Debug)]
pub enum AppError {
    #[error(transparent)]
    Domain(#[from] DomainError),
    #[error(transparent)]
    Infra(#[from] InfraError),
}

パターン2:HTTPステータスコードとの対応

use axum::http::StatusCode;

#[derive(Error, Debug)]
pub enum ApiError {
    #[error("リソースが見つかりません")]
    NotFound,
    #[error("認証が必要です")]
    Unauthorized,
    #[error("リクエストが不正です: {0}")]
    BadRequest(String),
    #[error("内部エラー")]
    Internal(#[from] anyhow::Error),
}

impl ApiError {
    pub fn status_code(&self) -> StatusCode {
        match self {
            ApiError::NotFound => StatusCode::NOT_FOUND,
            ApiError::Unauthorized => StatusCode::UNAUTHORIZED,
            ApiError::BadRequest(_) => StatusCode::BAD_REQUEST,
            ApiError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
        }
    }
}

まとめ

早見表

項目 thiserror anyhow
用途 カスタムエラー型定義 楽なエラーハンドリング
適した場面 ライブラリ アプリケーション
パターンマッチ しやすい しにくい
コンテキスト追加 手動 .context()
学習コスト やや高

チェックリスト

  • ライブラリを書くなら thiserror でエラー型を定義
  • アプリケーションなら anyhow で楽をする
  • 両方使うのもアリ
  • context() でエラーに情報を追加する習慣をつける

今すぐできるアクション

  1. 既存プロジェクトのエラー処理を見直す
  2. anyhow::Context でエラーメッセージを改善
  3. ライブラリを作るときは thiserror を検討

結論:「宗教戦争」じゃなくて、用途で使い分ける。

ライブラリ → thiserror、アプリ → anyhow。両方使ってもいい。

みなさんも良いエラーハンドリングライフを!

この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!

8
2
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
8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?