blog.dopana

Back

Rustにおけるエラー処理は、多くの一般的なプログラミング言語と大きく異なります。Rustには try/catch による例外機構や null ポインタが存在しません。その代わりに、型安全で明示的、かつコンパイル時に検証可能なエラー処理モデルを採用しています。

10歳でもわかる説明:郵便配達サービス#

Rustの関数呼び出しは、郵便配達で荷物を届けるプロセスに例えられます。

  1. 正常配達 (Ok(荷物)): 配達員がインターホンを鳴らし、注文した荷物を無事に手渡してくれます。
  2. 回復可能な配達トラブル (Err(理由)): 住所の番地が抜けていたり受取人が不在でした。郵便局は差出人に通知を戻し、再配達・電話連絡・転送などの対応策を委ねます。
  3. 致命的事故 (panic!): 配達トラックが全焼した、あるいは郵便局の建物自体が倒壊しました。安全に処理を継続する手段がないため、全体の処理を即座に強制終了します。
graph TD
    Op["処理 / 関数呼び出し"] --> Choice{"回復可能なエラーか?"}
    Choice -->|No: 致命的バグ / 不変条件の破壊| Panic["panic! マクロ<br/>(クラッシュ, スタック展開, 強制終了)"]
    Choice -->|Yes: ファイル不在, 通信失敗, 不正入力| ResultEnum["Result&lt;T, E&gt;"]
    ResultEnum --> OkBranch["Ok(値) -> 成功処理"]
    ResultEnum --> ErrBranch["Err(エラー) -> 処理または ? で上位へ伝播"]

Rustにおける2つのエラー分類#

特徴回復可能なエラー (Result<T, E>)回復不能なエラー (panic!)
メカニズム列挙型 Result<T, E> (Ok または Err)マクロ panic!("...")
主な発生要因ファイル不在、文字列パース失敗、通信タイムアウト配列の境界外アクセス、アサーション失敗
処理方法matchif let? 演算子、コンビネータプロセスの即時終了またはスタック展開
設計思想アプリケーションの想定される通常フローの一部深刻なバグであり、安全な継続が不可能

panic! による回復不能なエラー#

panic! マクロが実行されると:

  1. エラーメッセージが標準エラー出力(stderr)に出力されます。
  2. デフォルトではスタックの巻き戻し(unwind)を行い、メモリやリソースを安全にクリーンアップします。
  3. ゼロ以外の終了コードでプロセスが停止します。
src/main.rs
fn main() {
    // 明示的なパニック
    // panic!("致命的なシステム障害:データベース接続プールが破損しました");

    // 実行時チェックによる自動パニック
    let numbers = vec![10, 20, 30];
    let _item = numbers[99]; // panics: index out of bounds: the len is 3 but the index is 99
}
rust

[!NOTE] ターミナルで RUST_BACKTRACE=1 環境変数を設定して実行すると、パニック発生箇所までの完全なコールスタックトレースが表示されます。

Result<T, E> による回復可能なエラー#

Rustの標準ライブラリでは、Result は以下のように定義されています:

enum Result<T, E> {
    Ok(T),
    Err(E),
}
rust

match を使った Result のパターンマッチ#

アンラップのショートカット: unwrapexpect#

プロトタイプ開発やテストコードにおいて、Result は値を簡易に取り出すメソッドを提供しています:

src/main.rs
use std::fs::File;

// 1. .unwrap() -> Okなら値を返し、Errなら汎用メッセージでpanic
let f1 = File::open("config.json").unwrap();

// 2. .expect(msg) -> Okなら値を返し、Errなら指定メッセージでpanic
let f2 = File::open("config.json").expect("config.json はアプリケーション起動に必須です");
rust

[!TIP] 本番コードでは常に .unwrap() よりも .expect() を優先してください。詳細なエラーコンテキストを残すことで、ログ解析の時間を大幅に削減できます。

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

毎回 match を書いてエラーを返す代わりに、? 演算子を使うことで Ok(T) の値を取り出すか、Err(E) の場合に即座に関数からリターンさせることができます。

sequenceDiagram
    participant Caller as 呼び出し元関数
    participant Worker as read_username
    participant FS as ファイルシステム

    Worker->>FS: File::open("username.txt")
    alt ファイルが存在
        FS-->>Worker: Ok(file)
        Worker->>FS: read_to_string()
        alt 読み込み成功
            FS-->>Worker: Ok(bytes)
            Worker-->>Caller: Ok(username)
        else 読み込み失敗
            FS-->>Worker: Err(io_error)
            Worker-->>Caller: ? 経由で Err(io_error) を即時返却
        end
    else ファイルが存在しない
        FS-->>Worker: Err(io_error)
        Worker-->>Caller: ? 経由で Err(io_error) を即時返却
    end

実装比較#

? 演算子の内部自動型変換#

? 演算子はエラー値に対して内部的に From::from を自動呼び出しし、関数の戻り値型で指定されたエラー型へと変換します。

graph LR
    Expr["Result&lt;T, E1&gt; を評価"] --> Check{"Ok か Err か?"}
    Check -->|Ok value| Extract["値を取り出して次の処理へ進む"]
    Check -->|Err e1| Convert["From::from(e1) で E2 に型変換"]
    Convert --> ReturnEarly["関数から Err(e2) を即時リターン"]

カスタムエラー型の定義#

実際のアプリケーションでは、1つの関数内で複数種類のエラー(I/Oエラー、数値パースエラー、DBエラーなど)が発生します。これらは列挙型(Enum)としてまとめるのが標準的です。

ResultOption の関数型コンビネータ#

Rustは、エラーや値をスマートに変形するためのメソッド群を備えています:

  • .map(f): Ok(v) の値に関数 f を適用し、Err の場合はそのまま通過させます。
  • .and_then(f): Result を返す後続の処理をチェーンします。
  • .unwrap_or(default): 値を取り出し、Err の場合は指定したデフォルト値を返します。
  • .unwrap_or_else(f): クロージャを用いて遅延評価でデフォルト値を生成します。
src/main.rs
fn get_port_or_default() -> u16 {
    std::env::var("PORT")
        .ok()
        .and_then(|p| p.parse::<u16>().ok())
        .unwrap_or(8080)
}
rust

まとめ#

  • 復旧不可能な致命的バグには panic! を用います。
  • 通常運用で起こりうる失敗には Result<T, E> を用い、呼び出し側に適切なハンドリングを委ねます。
  • ? 演算子により、ゼロコストで簡潔なエラー伝播と自動型変換が実現できます。
  • カスタムエラーEnumを定義することで、多様なエラーを統一的かつ堅牢に管理できます。

参考資料#