Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

エラーハンドリング応用

実践的なエラー処理のパターンを学びます。

? 演算子

エラーを呼び出し元に伝播させます。

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};

// ? を使わない場合
fn read_file_verbose(path: &str) -> Result<String, io::Error> {
    let file = match File::open(path) {
        Ok(f) => f,
        Err(e) => return Err(e),
    };

    let mut contents = String::new();
    match file.read_to_string(&mut contents) {
        Ok(_) => Ok(contents),
        Err(e) => Err(e),
    }
}

// ? を使う場合(簡潔!)
fn read_file(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)
}
}

? の動作

#![allow(unused)]
fn main() {
let value = some_result?;

// は以下と同等
let value = match some_result {
    Ok(v) => v,
    Err(e) => return Err(e.into()),
};
}

チェーン

#![allow(unused)]
fn main() {
fn read_file(path: &str) -> Result<String, io::Error> {
    let mut contents = String::new();
    File::open(path)?.read_to_string(&mut contents)?;
    Ok(contents)
}

// さらに簡潔に
fn read_file_short(path: &str) -> Result<String, io::Error> {
    std::fs::read_to_string(path)
}
}

カスタムエラー型

基本的なカスタムエラー

#![allow(unused)]
fn main() {
#[derive(Debug)]
enum AppError {
    NotFound(String),
    InvalidInput(String),
    IoError(std::io::Error),
}

impl std::fmt::Display for AppError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            AppError::NotFound(msg) => write!(f, "Not found: {}", msg),
            AppError::InvalidInput(msg) => write!(f, "Invalid input: {}", msg),
            AppError::IoError(e) => write!(f, "IO error: {}", e),
        }
    }
}

impl std::error::Error for AppError {}
}

From トレイトで変換を自動化

#![allow(unused)]
fn main() {
impl From<std::io::Error> for AppError {
    fn from(error: std::io::Error) -> Self {
        AppError::IoError(error)
    }
}

// これで ? が自動変換してくれる
fn do_something() -> Result<(), AppError> {
    let _file = std::fs::File::open("file.txt")?;  // io::Error -> AppError
    Ok(())
}
}

thiserror クレート(推奨)

カスタムエラーを簡単に定義できます。

Cargo.toml

[dependencies]
thiserror = "1.0"
#![allow(unused)]
fn main() {
use thiserror::Error;

#[derive(Error, Debug)]
enum AppError {
    #[error("User not found: {0}")]
    NotFound(String),

    #[error("Invalid input: {0}")]
    InvalidInput(String),

    #[error("Database error")]
    DatabaseError(#[from] sqlx::Error),

    #[error("IO error")]
    IoError(#[from] std::io::Error),
}
}

anyhow クレート(アプリケーション向け)

エラー型を気にせず簡単にエラー処理できます。

Cargo.toml

[dependencies]
anyhow = "1.0"
use anyhow::{Result, Context};

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

fn main() -> Result<()> {
    let config = read_config()?;
    println!("{}", config);
    Ok(())
}

エラー処理のパターン

パターン1: 早期リターン

#![allow(unused)]
fn main() {
fn process_user(id: u32) -> Result<User, AppError> {
    if id == 0 {
        return Err(AppError::InvalidInput("ID cannot be 0".into()));
    }

    let user = find_user(id)?;

    if !user.is_active {
        return Err(AppError::NotFound("User is inactive".into()));
    }

    Ok(user)
}
}

パターン2: map_err で変換

#![allow(unused)]
fn main() {
fn read_config() -> Result<Config, AppError> {
    let content = std::fs::read_to_string("config.toml")
        .map_err(|e| AppError::IoError(e))?;

    toml::from_str(&content)
        .map_err(|e| AppError::InvalidInput(e.to_string()))
}
}

パターン3: ok_or / ok_or_else

#![allow(unused)]
fn main() {
fn get_user(id: u32) -> Result<User, AppError> {
    users.get(&id)
        .cloned()
        .ok_or_else(|| AppError::NotFound(format!("User {} not found", id)))
}
}

パターン4: and_then でチェーン

#![allow(unused)]
fn main() {
fn process() -> Result<String, AppError> {
    read_file("input.txt")
        .and_then(|content| parse_content(&content))
        .and_then(|data| transform_data(data))
        .and_then(|result| save_result(result))
}
}

main関数でのエラー処理

use std::process;

fn main() {
    if let Err(e) = run() {
        eprintln!("エラー: {}", e);
        process::exit(1);
    }
}

fn run() -> Result<(), Box<dyn std::error::Error>> {
    let config = read_config()?;
    let data = process_data(&config)?;
    save_output(&data)?;
    Ok(())
}

パニックとの使い分け

状況使うもの
回復可能なエラーResult<T, E>
プログラムのバグpanic!
テストunwrap(), expect()
プロトタイプunwrap()
本番コード適切なエラー処理
#![allow(unused)]
fn main() {
// panic! を使う場面(プログラムのバグ)
fn get_element(index: usize) -> &str {
    let items = ["a", "b", "c"];
    items.get(index).expect("Index should be valid")
}

// Result を使う場面(回復可能なエラー)
fn find_user(id: u32) -> Result<User, AppError> {
    // ...
}
}

まとめ

概念説明
?エラーを伝播
カスタムエラーアプリ固有のエラー型
thiserrorエラー型定義を簡単に
anyhow柔軟なエラー処理
map_errエラー型を変換
contextエラーに文脈を追加

確認テスト

Q1. ?演算子の動作は?

Q2. thiserroranyhowの使い分けは?

Q3. map_errの役割は?

Q4. 以下のコードを?で簡潔にした結果は?
let content = match std::fs::read_to_string(path) { Ok(c) => c, Err(e) => return Err(e) };

Q5. panic!を使うべき場面は?


Phase 3 完了!

おめでとうございます!Phase 3を完了しました。

学んだこと:

  • 構造体(struct、impl)
  • 列挙型とパターンマッチング
  • トレイト
  • ジェネリクス
  • モジュールシステム
  • エラーハンドリング応用

次のPhase: Phase 4: エコシステムと実践