Rustのエラー処理を極める: Result、Option、Panicと?演算子
Rustのエラー処理を徹底解説:回復不能なpanic!、回復可能なResult<T, E>、?伝播演算子、カスタムエラー型の定義、ベストプラクティス。
Rustにおけるエラー処理は、多くの一般的なプログラミング言語と大きく異なります。Rustには try/catch による例外機構や null ポインタが存在しません。その代わりに、型安全で明示的、かつコンパイル時に検証可能なエラー処理モデルを採用しています。
10歳でもわかる説明:郵便配達サービス#
Rustの関数呼び出しは、郵便配達で荷物を届けるプロセスに例えられます。
- 正常配達 (
Ok(荷物)): 配達員がインターホンを鳴らし、注文した荷物を無事に手渡してくれます。 - 回復可能な配達トラブル (
Err(理由)): 住所の番地が抜けていたり受取人が不在でした。郵便局は差出人に通知を戻し、再配達・電話連絡・転送などの対応策を委ねます。 - 致命的事故 (
panic!): 配達トラックが全焼した、あるいは郵便局の建物自体が倒壊しました。安全に処理を継続する手段がないため、全体の処理を即座に強制終了します。
graph TD
Op["処理 / 関数呼び出し"] --> Choice{"回復可能なエラーか?"}
Choice -->|No: 致命的バグ / 不変条件の破壊| Panic["panic! マクロ<br/>(クラッシュ, スタック展開, 強制終了)"]
Choice -->|Yes: ファイル不在, 通信失敗, 不正入力| ResultEnum["Result<T, E>"]
ResultEnum --> OkBranch["Ok(値) -> 成功処理"]
ResultEnum --> ErrBranch["Err(エラー) -> 処理または ? で上位へ伝播"]
Rustにおける2つのエラー分類#
| 特徴 | 回復可能なエラー (Result<T, E>) | 回復不能なエラー (panic!) |
|---|---|---|
| メカニズム | 列挙型 Result<T, E> (Ok または Err) | マクロ panic!("...") |
| 主な発生要因 | ファイル不在、文字列パース失敗、通信タイムアウト | 配列の境界外アクセス、アサーション失敗 |
| 処理方法 | match、if let、? 演算子、コンビネータ | プロセスの即時終了またはスタック展開 |
| 設計思想 | アプリケーションの想定される通常フローの一部 | 深刻なバグであり、安全な継続が不可能 |
panic! による回復不能なエラー#
panic! マクロが実行されると:
- エラーメッセージが標準エラー出力(
stderr)に出力されます。 - デフォルトではスタックの巻き戻し(unwind)を行い、メモリやリソースを安全にクリーンアップします。
- ゼロ以外の終了コードでプロセスが停止します。
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),
}rustmatch を使った Result のパターンマッチ#
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("ファイルの作成中に問題が発生: {e:?}"),
},
other_error => {
panic!("ファイルのオープン中に問題が発生: {other_error:?}");
}
},
};
println!("ファイルを正常に開きました: {greeting_file:?}");
}rustアンラップのショートカット: unwrap と expect#
プロトタイプ開発やテストコードにおいて、Result は値を簡易に取り出すメソッドを提供しています:
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
実装比較#
use std::fs::File;
use std::io::{self, Read};
// match を使った冗長な書き方
fn read_username_verbose() -> Result<String, io::Error> {
let username_file_result = File::open("username.txt");
let mut username_file = match username_file_result {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut username = String::new();
match username_file.read_to_string(&mut username) {
Ok(_) => Ok(username),
Err(e) => Err(e),
}
}
// ? 演算子を使った簡潔な書き方
fn read_username_concise() -> Result<String, io::Error> {
let mut username = String::new();
File::open("username.txt")?.read_to_string(&mut username)?;
Ok(username)
}rust? 演算子の内部自動型変換#
? 演算子はエラー値に対して内部的に From::from を自動呼び出しし、関数の戻り値型で指定されたエラー型へと変換します。
graph LR
Expr["Result<T, E1> を評価"] --> Check{"Ok か Err か?"}
Check -->|Ok value| Extract["値を取り出して次の処理へ進む"]
Check -->|Err e1| Convert["From::from(e1) で E2 に型変換"]
Convert --> ReturnEarly["関数から Err(e2) を即時リターン"]
カスタムエラー型の定義#
実際のアプリケーションでは、1つの関数内で複数種類のエラー(I/Oエラー、数値パースエラー、DBエラーなど)が発生します。これらは列挙型(Enum)としてまとめるのが標準的です。
use std::fmt;
use std::io;
use std::num::ParseIntError;
#[derive(Debug)]
pub enum AppError {
Io(io::Error),
Parse(ParseIntError),
NotFound(String),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::Io(err) => write!(f, "I/Oエラー: {err}"),
AppError::Parse(err) => write!(f, "パースエラー: {err}"),
AppError::NotFound(item) => write!(f, "対象が見つかりません: {item}"),
}
}
}
impl std::error::Error for AppError {}
// ? 演算子による自動変換の実装
impl From<io::Error> for AppError {
fn from(err: io::Error) -> Self {
AppError::Io(err)
}
}
impl From<ParseIntError> for AppError {
fn from(err: ParseIntError) -> Self {
AppError::Parse(err)
}
}
fn process_port_config() -> Result<u16, AppError> {
let mut content = String::new();
std::fs::File::open("port.txt")?.read_to_string(&mut content)?; // io::Error -> AppError
let port: u16 = content.trim().parse()?; // ParseIntError -> AppError
Ok(port)
}rustResult と Option の関数型コンビネータ#
Rustは、エラーや値をスマートに変形するためのメソッド群を備えています:
.map(f):Ok(v)の値に関数fを適用し、Errの場合はそのまま通過させます。.and_then(f):Resultを返す後続の処理をチェーンします。.unwrap_or(default): 値を取り出し、Errの場合は指定したデフォルト値を返します。.unwrap_or_else(f): クロージャを用いて遅延評価でデフォルト値を生成します。
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を定義することで、多様なエラーを統一的かつ堅牢に管理できます。