はじめに
2025年ももうすぐ終わりますね.今年も様々なインシデントが発生しました.
私の記憶に新しいのは,先月のCloudflareの障害です.
なにやらRust製サービス内でのunwrap()により発生したpanicがトリガーだとか.
ちょうど私の周囲でもRustの勉強をしてくれる人が増えてきたので,これを機にRustにおけるエラーハンドリングについてまとめれば彼らの勉強にも役立つのではないかと思い,この記事を書いています.
前提として:OptionとResult
RustにはOptionとResultという列挙型が存在します.
念のため軽く触れておきます.
enum Option<T> {
None,
Some(T),
}
enum Result<T, E> {
Ok(T),
Err(E),
}
Option<T>は値がある/ない可能性を型として設計に組み込んだもので,Nullの概念の代替と言えるでしょう.Noneは値がないことを示し,Some(T)は値が存在することを示します.
Result<T, E>は処理の成否を表すもので,Ok(T)が成功時,Err(E)がエラー時の値を保持します.例外のないRustにおける標準的なエラーハンドリングの仕組みです.
Rustにおけるエラーハンドリング
それを踏まえ,ここからはRustにおけるエラーハンドリングをいくつか紹介します.
1. unwrap - 強制的な取り出し
冒頭でも触れたunwrapです.
let x = v.get(0).unwrap();
unwrap()はOptionやResultから中身を強制的に取り出します.
しかし,NoneやErrの場合はpanicを起こします.
thread 'main' panicked at 'called `Option::unwrap()` on a `None` value'
つまり,unwrap()が呼び出された際,エラーケースでは通常スレッドを終了させます.
(Optionでのunwrap()ってぬるぽみたいだな...)
個人的には,テストコード内やプロトタイピング段階,その処理が必ず成功することが保証されている場合などで使用することを推奨します.
本番環境で動作するコード内での使用はあまりおすすめしません.
2. match - 明示的なハンドリング
matchを使った明示的なエラーハンドリングです.
match File::open("hoge.txt") {
Ok(file) => {
// 成功時
println!("ファイルを開きました");
}
Err(e) => {
// エラー時
eprintln!("エラー: {}", e);
}
}
これが最も素直な方法のような気がします.
戻り値に応じた処理を明示的に記述できるため,エラーケースの見落としを防ぎ,処理のフローが明確になります.
全てのケースを網羅的に処理できる,コンパイラが処理漏れをチェックしてくれる,エラー時の動作が一目で分かる,みたいなメリットがあると思いますが,コードが冗長になりやすい印象も受けます.
3. ? - エラーの伝播
?を用いたエラーの伝播です.
fn read_file() -> Result<String, std::io::Error> {
let mut s = String::new();
let mut file = File::open("hoge.txt")?;
file.read_to_string(&mut s)?;
Ok(s)
}
?は以下のような動作をします.
-
Ok(T)→Tを取り出して処理を続行 -
Err(E)→ 関数をreturnし、エラーを上位の呼び出し元に伝播
これにより,ローカルでは処理できないエラーは上位に任せ,対処すべき場所でだけエラーハンドリングするという設計がしやすくなります.
可読性も高いので,?はよく使用されている印象です.
4. if let - 簡潔なパターンマッチ
if letは上述したmatchの簡易版のようなものです.
if let Ok(file) = File::open("hoge.txt") {
// 成功時
println!("ファイルを開きました");
}
// エラー時は何もしない
ログだけ出す,値がなければ何もしないなど,エラーを無視してよいケースではmatchよりも可読性が高くなります.
これは反駁可能なパターン(処理が失敗する可能性があるパターン)を束縛できる構文です.ただし,重要なエラーを握りつぶしていないか注意する必要があります.
明示的にエラーを無視する意図がある場合での使用を推奨します.
反駁可能なlet文であるlet elseという個人的にかなり好きな構文もあるのですが,ここでは割愛します.
5. その他
expect - メッセージ付きunwrap
expectは上述したunwrapにカスタムメッセージを付けたものです.
let config = get_config("database_url")
.expect("DATABASE_URLが設定されていません。環境変数を確認してください");
エラー時には指定したメッセージとともにpanicするため,デバッグ時にエラーの原因を特定しやすくなります.
unwrap_or,unwrap_or_else,unwrap_or_default
unwrap_or,unwrap_or_else,unwrap_or_defaultは,エラー時にデフォルト値を使って処理を続けるもので,panicを起こしません.
unwrap_or
let timeout = get_config("timeout").unwrap_or(30);
// エラーなら30を使用
固定のデフォルト値を指定します.
unwrap_or_else
let cache = get_cache().unwrap_or_else(|| {
// エラー時のみ実行される
println!("キャッシュがないので新規作成します");
create_new_cache()
});
unwrap_or_elseは遅延評価されます.
デフォルト値を計算によって生成する場合に使います.
unwrap_or_default
let items = get_items().unwrap_or_default();
// エラーなら空のVecが返る
Defaultトレイトを実装している型に対しては使用可能です(数値なら0、Stringなら""など).
まとめ
Rustにおけるエラーハンドリングの基本的な手法を紹介しました.
適切なエラーハンドリングはコードの可読性を上げるだけでなく,システムの堅牢性にも大きく影響します.
障害を避けるためには,状況に応じてこれらを使い分けることが重要です.
エラーハンドリングはRustの強力な機能の一つです.適切に使いこなして,安全で信頼性の高いシステムを構築しましょう.