2026年版 Rustエラーハンドリング完全ガイド: Result、Option、thiserror、anyhowの実践的活用法

Rustのエラーハンドリングを徹底解説。Result型とOption型の基本から、thiserrorとanyhowクレートの使い分け、?演算子の活用まで、実践的なコード例とともに学ぶ2026年最新ガイド。

2026年版 Rustエラーハンドリング完全ガイド: Result、Option、thiserror、anyhowの実践的活用法

Rustは、メモリ安全性と並行性の保証で知られるシステムプログラミング言語であり、そのエラーハンドリングシステムは言語設計の中核をなしています。2026年現在、Rustエコシステムは成熟し、エラー処理のベストプラクティスも確立されてきました。本記事では、Rustにおけるエラーハンドリングの基礎から応用まで、実践的なアプローチで解説します。

Rustのエラーハンドリングは、コンパイル時にエラーの可能性を強制的に処理させることで、実行時の予期せぬクラッシュを防ぎます。Result型とOption型を理解することが、堅牢なRustコードを書く第一歩です。

Result型とOption型の基本

Rustのエラーハンドリングは、例外機構ではなく、型システムを活用したアプローチを採用しています。Result<T, E>型とOption<T>型は、Rustの標準ライブラリに定義されている列挙型であり、エラーや値の不在を明示的に表現します。

Result型の定義と使用方法

rust
use std::fs::File;
use std::io::{self, Read};

fn read_file_contents(path: &str) -> Result<String, io::Error> {
    let mut file = File::open(path)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

fn main() {
    match read_file_contents("config.txt") {
        Ok(contents) => println!("File contents: {}", contents),
        Err(e) => eprintln!("Error reading file: {}", e),
    }
}

Result<T, E>は成功時にOk(T)を、失敗時にErr(E)を返します。この明示的な型により、呼び出し側はエラーの可能性を無視できません。

Option型による値の不在の表現

rust
fn find_user_by_id(users: &[User], id: u64) -> Option<&User> {
    users.iter().find(|user| user.id == id)
}

struct User {
    id: u64,
    name: String,
}

fn main() {
    let users = vec![
        User { id: 1, name: String::from("Alice") },
        User { id: 2, name: String::from("Bob") },
    ];
    
    match find_user_by_id(&users, 1) {
        Some(user) => println!("Found user: {}", user.name),
        None => println!("User not found"),
    }
}

Option<T>は、値が存在する場合はSome(T)を、存在しない場合はNoneを返します。nullポインタの問題を型システムレベルで解決しています。

?演算子によるエラー伝播

?演算子は、Rustのエラーハンドリングにおいて最も重要な機能の一つです。この演算子により、エラー処理のボイラープレートコードを大幅に削減できます。

rust
use std::fs::File;
use std::io::{self, Read, Write};

fn copy_file_contents(src: &str, dst: &str) -> Result<(), io::Error> {
    let mut source = File::open(src)?;
    let mut destination = File::create(dst)?;
    
    let mut buffer = Vec::new();
    source.read_to_end(&mut buffer)?;
    destination.write_all(&buffer)?;
    
    Ok(())
}

?演算子は、ResultOkの場合は内部の値を取り出し、Errの場合は即座に関数から戻ります。従来のmatch式やtry!マクロと比較して、コードの可読性が大幅に向上します。

?演算子とFrom trait

rust
use std::fs::File;
use std::io::{self, Read};
use std::num::ParseIntError;

#[derive(Debug)]
enum ConfigError {
    Io(io::Error),
    Parse(ParseIntError),
}

impl From<io::Error> for ConfigError {
    fn from(err: io::Error) -> ConfigError {
        ConfigError::Io(err)
    }
}

impl From<ParseIntError> for ConfigError {
    fn from(err: ParseIntError) -> ConfigError {
        ConfigError::Parse(err)
    }
}

fn read_config_value(path: &str) -> Result<i32, ConfigError> {
    let mut file = File::open(path)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    let value: i32 = contents.trim().parse()?;
    Ok(value)
}

?演算子はFrom traitを活用してエラー型の自動変換を行います。これにより、異なるエラー型を統一的に扱うことが可能になります。

thiserrorによるカスタムエラー型の定義

thiserrorクレートは、カスタムエラー型の定義を簡潔にするためのderiveマクロを提供します。2026年現在、ライブラリ開発においてはthiserrorがデファクトスタンダードとなっています。

rust
use thiserror::Error;
use std::io;

#[derive(Error, Debug)]
pub enum DatabaseError {
    #[error("Connection failed: {0}")]
    ConnectionFailed(String),
    
    #[error("Query execution failed: {query}")]
    QueryFailed {
        query: String,
        #[source]
        source: io::Error,
    },
    
    #[error("Record not found: id={id}")]
    NotFound { id: u64 },
    
    #[error(transparent)]
    Io(#[from] io::Error),
}

fn execute_query(query: &str) -> Result<Vec<String>, DatabaseError> {
    if query.is_empty() {
        return Err(DatabaseError::QueryFailed {
            query: query.to_string(),
            source: io::Error::new(io::ErrorKind::InvalidInput, "Empty query"),
        });
    }
    
    Ok(vec![String::from("result1"), String::from("result2")])
}

thiserrorの主な特徴は以下の通りです:

  • #[error("...")]属性によるDisplay traitの自動実装
  • #[from]属性によるFrom traitの自動実装
  • #[source]属性によるエラーチェーンのサポート
  • #[transparent]属性による内部エラーの透過的な表示

anyhowによるアプリケーションレベルのエラーハンドリング

anyhowクレートは、アプリケーション開発において柔軟なエラーハンドリングを提供します。ライブラリではなくアプリケーション(バイナリ)コードでの使用に適しています。

rust
use anyhow::{Context, Result, bail, ensure};
use std::fs::File;
use std::io::Read;

fn load_configuration(path: &str) -> Result<Config> {
    ensure!(!path.is_empty(), "Configuration path cannot be empty");
    
    let mut file = File::open(path)
        .with_context(|| format!("Failed to open configuration file: {}", path))?;
    
    let mut contents = String::new();
    file.read_to_string(&mut contents)
        .context("Failed to read configuration file")?;
    
    let config: Config = serde_json::from_str(&contents)
        .context("Failed to parse configuration JSON")?;
    
    if config.timeout == 0 {
        bail!("Invalid configuration: timeout must be greater than 0");
    }
    
    Ok(config)
}

#[derive(serde::Deserialize)]
struct Config {
    timeout: u64,
    host: String,
}

anyhowの主要な機能:

  • Result<T>型エイリアス(Result<T, anyhow::Error>
  • context()with_context()によるエラーメッセージの追加
  • bail!マクロによる即時エラー返却
  • ensure!マクロによる条件チェック
  • 任意のエラー型をラップ可能

thiserrorとanyhowの使い分け

両クレートは補完的な関係にあり、適切な場面で使い分けることが重要です。

rust
// ライブラリコード (thiserror使用)
mod library {
    use thiserror::Error;
    
    #[derive(Error, Debug)]
    pub enum LibraryError {
        #[error("Invalid input: {0}")]
        InvalidInput(String),
        
        #[error("Processing failed")]
        ProcessingFailed(#[source] std::io::Error),
    }
    
    pub fn process_data(data: &[u8]) -> Result<Vec<u8>, LibraryError> {
        if data.is_empty() {
            return Err(LibraryError::InvalidInput("Data cannot be empty".into()));
        }
        Ok(data.to_vec())
    }
}

// アプリケーションコード (anyhow使用)
mod application {
    use anyhow::{Context, Result};
    use super::library;
    
    pub fn run_application() -> Result<()> {
        let data = std::fs::read("input.bin")
            .context("Failed to read input file")?;
        
        let processed = library::process_data(&data)
            .context("Data processing failed")?;
        
        std::fs::write("output.bin", &processed)
            .context("Failed to write output file")?;
        
        Ok(())
    }
}

使い分けの指針:

| 用途 | 推奨クレート | 理由 | |------|-------------|------| | ライブラリ開発 | thiserror | 具体的なエラー型を提供し、利用者が適切にハンドリング可能 | | アプリケーション開発 | anyhow | 柔軟なエラー伝播とコンテキスト追加が容易 | | CLI ツール | anyhow | エラーメッセージの表示が簡潔 | | Web API | thiserror + anyhow | ドメインエラーはthiserror、ハンドラではanyhow |

エラーハンドリングのベストプラクティス

2026年のRustエコシステムにおいて、以下のプラクティスが推奨されています。

パニックを避ける

rust
// 避けるべきパターン
fn get_first_element_bad(vec: &[i32]) -> i32 {
    vec[0] // パニックの可能性
}

// 推奨パターン
fn get_first_element_good(vec: &[i32]) -> Option<i32> {
    vec.first().copied()
}

// または Result を返す
fn get_first_element_result(vec: &[i32]) -> Result<i32, &'static str> {
    vec.first().copied().ok_or("Vector is empty")
}

エラーチェーンの活用

rust
use anyhow::{Context, Result};
use std::fs::File;
use std::io::Read;

fn load_user_preferences(user_id: u64) -> Result<Preferences> {
    let path = format!("/home/{}/.config/app/preferences.json", user_id);
    
    let mut file = File::open(&path)
        .with_context(|| format!("Failed to open preferences for user {}", user_id))?;
    
    let mut contents = String::new();
    file.read_to_string(&mut contents)
        .with_context(|| format!("Failed to read preferences file: {}", path))?;
    
    serde_json::from_str(&contents)
        .with_context(|| "Failed to parse preferences JSON")
}

#[derive(serde::Deserialize)]
struct Preferences {
    theme: String,
}

型エイリアスの活用

rust
use thiserror::Error;

pub type Result<T> = std::result::Result<T, AppError>;

#[derive(Error, Debug)]
pub enum AppError {
    #[error("Database error: {0}")]
    Database(#[from] DatabaseError),
    
    #[error("Network error: {0}")]
    Network(#[from] NetworkError),
    
    #[error("Configuration error: {0}")]
    Config(String),
}

#[derive(Error, Debug)]
pub enum DatabaseError {
    #[error("Connection lost")]
    ConnectionLost,
}

#[derive(Error, Debug)]
pub enum NetworkError {
    #[error("Timeout")]
    Timeout,
}

Rustの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

まとめ

Rustのエラーハンドリングシステムは、型安全性とエルゴノミクスを両立させた設計となっています。Result型とOption型を基盤として、?演算子による簡潔なエラー伝播、thiserrorによる型安全なカスタムエラー定義、anyhowによる柔軟なアプリケーションレベルのエラーハンドリングを組み合わせることで、堅牢で保守性の高いコードを実現できます。

ライブラリ開発ではthiserrorを使用して具体的なエラー型を提供し、アプリケーション開発ではanyhowを活用してエラーコンテキストを充実させることが、2026年現在のベストプラクティスです。これらのツールを適切に使い分けることで、Rustの強力な型システムを最大限に活かしたエラーハンドリングが実現できます。

共有

関連記事