10. エラー処理: panic! と Result

🎯 この章のゴール

  • 回復不能なエラー(panic!)と回復可能なエラー(Result)を使い分ける
  • ? 演算子でエラーをスッキリ伝播させる

💡 ひとことで言うと

Rust に例外はありません。失敗するかもしれない関数は 戻り値の型で「失敗するかも」と宣言 します。無視するとコンパイラに注意されます。

panic!: 回復不能なエラー

fn main() {
    let v = vec![1, 2, 3];
    v[99]; // thread 'main' panicked at ... index out of bounds
}
Playground で開く ↗

panic! はプログラムを停止します。バグ(ありえない状態)のときに使います。 RUST_BACKTRACE=1 cargo run で呼び出し履歴を表示できます。

Result: 回復可能なエラー

// enum Result<T, E> { Ok(T), Err(E) }
use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let f = match File::open("hello.txt") {
        Ok(file) => file,
        Err(e) if e.kind() == ErrorKind::NotFound => {
            println!("ファイルがないので作ります");
            File::create("hello.txt").expect("作成に失敗")
        }
        Err(e) => panic!("開けません: {e:?}"),
    };
    println!("{f:?}");
}
Playground で開く ↗

? 演算子: エラーの伝播

use std::fs;
use std::io;

// match で書くと…
fn read_name_verbose() -> Result<String, io::Error> {
    let s = match fs::read_to_string("name.txt") {
        Ok(s) => s,
        Err(e) => return Err(e),
    };
    Ok(s.trim().to_string())
}

// ? を使うと同じことが 1 行に!
fn read_name() -> Result<String, io::Error> {
    let s = fs::read_to_string("name.txt")?;
    Ok(s.trim().to_string())
}

fn main() {
    println!("{:?}", read_name_verbose().is_err());
    println!("{:?}", read_name());
}
Playground で開く ↗

? は Option を返す関数でも使えます(None なら即 None を返す)。

fn first_char_upper(s: &str) -> Option<char> {
    let c = s.chars().next()?;
    Some(c.to_ascii_uppercase())
}

fn main() {
    println!("{:?} {:?}", first_char_upper("rust"), first_char_upper(""));
}
Playground で開く ↗

main から Result を返す

use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
    let n: i32 = "42".parse()?;   // ParseIntError
    let text = std::fs::read_to_string("Cargo.toml").unwrap_or_default();
    println!("{n} {}", text.len());
    Ok(())
}
Playground で開く ↗

Box<dyn Error> は「何らかのエラー」を表す型で、異なる種類のエラーを ? でまとめて扱えます。

使い分けの指針

状況使うもの
呼び出し側が対処できる失敗(ファイルなし、入力ミス)Result を返す
バグ・ありえない状態panic!
サンプル・試作・テストunwrap() / expect() で OK
絶対に失敗しないと分かっているexpect("理由") で意図を残す

アプリケーションでは anyhow、ライブラリでは thiserror クレートがよく使われます。

🧪 やってみよう

下の書きかけコードの TODO を埋めて、▶ 実行 で確かめよう。todo!() のままだと実行時に panic します。

文字列 "3,5" を受け取り、2 つの数の和を返す関数を ? を使って書いてみよう。

use std::error::Error;

fn sum_pair(s: &str) -> Result<i32, Box<dyn Error>> {
    todo!("ヒント: s.split_once(',') と parse::<i32>()?")
}

fn main() {
    println!("{:?}", sum_pair("3,5").map_err(|e| e.to_string())); // Ok(8)
    println!("{:?}", sum_pair("3,x").map_err(|e| e.to_string())); // Err(...)
}
Playground で開く ↗
💡 解答例を見る
use std::error::Error;

fn sum_pair(s: &str) -> Result<i32, Box<dyn Error>> {
    // ok_or で Option を Result に。&str は Box<dyn Error> に自動変換される
    let (a, b) = s.split_once(',').ok_or("カンマがありません")?;
    Ok(a.trim().parse::<i32>()? + b.trim().parse::<i32>()?)
}

fn main() {
    println!("{:?}", sum_pair("3,5").map_err(|e| e.to_string())); // Ok(8)
    println!("{:?}", sum_pair("3,x").map_err(|e| e.to_string())); // Err(...)
}
Playground で開く ↗

📝 まとめ

  • 回復不能 → panic!、回復可能 → Result<T, E>
  • ? で「失敗なら早期リターン」を簡潔に
  • ? は Result / Option を返す関数の中でだけ使える

✅ 理解度クイズ

Q1
回復可能なエラー(ファイルが無い、入力が不正など)を表すのに使う型は?
Q2
let n = s.parse::<i32>()?; の ? が行うことは?
Q3
次のコードはコンパイルできる?
fn main() {
    let n: i32 = "42".parse()?;
    println!("{n}");
}
Q4
unwrap() と expect("msg") の違いは?