はじめに
Rust で Option や Result から値を取り出すとき、つい .unwrap() を書いてしまう場面があります。動くし短い。だから書く。だけどそのコードは、エラー時にスタックトレースなしで panic します。本番で踏むと 「どこで何の理由で死んだのか」 がわからない事故になります。
この記事は、.unwrap() を書きたくなった瞬間の代替案を8個並べます。状況に応じて選んでください。
前提: なぜ unwrap() は 良くないか
.unwrap() の問題は3つです。
- panic メッセージが「
called \Option::unwrap()` on a `None` value`」のような汎用文言で、原因の手がかりがない - どんな理由で None / Err が起きたかドキュメントに残らない
- 後で読み返したときに「ここは絶対 None にならないと信じている」のか「面倒で書いた」のか区別がつかない
Cargo.toml で clippy::unwrap_used = "deny" を入れると unwrap() がコンパイルエラーになります。これを入れたうえで以下の選択肢から選ぶのが、2024 edition のクリーンなコードベースでの基本姿勢です。
選択肢 1: ? で伝搬する
呼び出し側にエラーを投げ返すのが一番素直です。Result 型を返す関数の中で ? を1文字書くだけ。
fn read_config() -> Result<String, std::io::Error> { let s = std::fs::read_to_string("config.toml")?; Ok(s) }
この用途で unwrap() を書く理由は基本ありません。? の方が短く、エラー型を保ったまま呼び出し側に渡せます。
選択肢 2: expect("理由") で意図を残す
「絶対 None にならないと自分は信じているが、もし None になったら panic で気付きたい」時は .expect("理由") を使います。
let port = std::env::var("PORT").expect("PORT must be set in environment");
panic 時のメッセージが「PORT must be set in environment」になるので、ログを見た人がすぐ原因にたどりつきます。unwrap() で死んだ場合の「called Option::unwrap on a None value」とは情報量が桁違いです。
expect_used lint を warn にしておくと、expect を使った箇所が coverage 的に見えます。それでも unwrap よりは1段マシ。
選択肢 3: unwrap_or でデフォルト値を返す
「None / Err なら fallback 値を使う」だけのケース。unwrap_or 系がきれいに収まります。
let port = std::env::var("PORT").unwrap_or_else(|_| "8080".to_string()); let first_word = " hello world".split_whitespace().next().unwrap_or("(none)"); let count: u32 = config.get("count") .and_then(|s| s.parse().ok()) .unwrap_or(0);
unwrap_or (静的なデフォルト) と unwrap_or_else (クロージャで遅延評価するデフォルト) の使い分けがあります。unwrap_or(expensive_call()) だと None じゃなくても evaluate されて重いので、計算が必要な fallback は unwrap_or_else 一択です。
選択肢 4: ok_or / ok_or_else で Option を Result に変える
「Option<T> を ? で伝搬したい」場面。ok_or で Result<T, E> に持ち上げます。
#[derive(Debug)] enum Error { Missing(&'static str), } fn read_port(map: &std::collections::HashMap<&str, String>) -> Result<u16, Error> { let raw = map.get("port").ok_or(Error::Missing("port"))?; raw.parse().map_err(|_| Error::Missing("port")) }
unwrap() で「絶対あるはず」と書く代わりに、 ok_or で「無かった場合のエラー」を表明する。読み手にとって意図がはるかに明確です。
選択肢 5: let-else でガードを書く
Option を ? で投げ返すには大げさだが、ネストはしたくない、という場面。 let-else でガード句を書きます。
fn label(maybe_name: Option<&str>) -> Option<String> { let Some(name) = maybe_name else { return None; }; Some(format!("hello, {name}")) }
「失敗時は早期 return、成功時は以降長く処理を続ける」スタイルが書けます。unwrap() で取り出してから後続処理を書くより、意図が線形に読めます。
選択肢 6: if let chain で複数条件を平らにする
複数の Option がすべて Some だったときだけ何かしたい、という場面。 Rust 1.88 / 2024 edition の if let chain。
fn first_premium(items: &[String]) -> Option<&str> { if let Some(first) = items.first() && let Some(rest) = first.strip_prefix("premium-") { return Some(rest); } None }
「全部 Some なら本体に入る」が一直線で読めます。
選択肢 7: match で全 variant を網羅する
エラー variant ごとに違う対処をしたい時は、 match で網羅します。
match parse_port(input) { Ok(port) => println!("port = {port}"), Err(ParsePortError::TooLarge) => eprintln!("port out of range"), Err(ParsePortError::NotANumber(s)) => eprintln!("not a number: {s}"), }
unwrap() でひとまとめに panic させると、原因によって違う出口を作れません。これは エラー variant 単位でログレベルや復旧戦略を変えたい 場合に重要です。
選択肢 8: 設計を見直す
最も強力な選択肢。「そもそも Option / Result を返す必要があったのか」を問い直すことです。
// before: ゼロ割りを runtime で検出 fn page_count(total: usize, page_size: usize) -> Option<usize> { if page_size == 0 { None } else { Some(total.div_ceil(page_size)) } } // after: NonZero<usize> でコンパイル時に「ゼロでない」を保証 fn page_count(total: usize, page_size: std::num::NonZero<usize>) -> usize { total.div_ceil(page_size.get()) }
引数の型を絞れば Option を返す必要がそもそもなくなります。unwrap() を書きたくなったら 「型でこの不安を消せないか」 を一度考える価値があります。これが Rust ならではの強み。
チェックリスト
unwrap() を書きそうになったとき、上から順にチェックします。
Resultを返す関数なら →?- 絶対あるはずだが panic で気付きたい →
.expect("理由") - None / Err なら fallback でよい →
unwrap_or/unwrap_or_else OptionをResultに変えたい →ok_or/ok_or_else- 失敗時は早期 return →
let-else - 複数
Optionを AND で扱う →if letchain - エラー variant ごとに分岐したい →
match - 「そもそも
Optionを返す必要があるか」 → 型を絞る
Cargo.toml に unwrap_used = "deny" を入れて lint を強制すれば、「とりあえず .unwrap()」を CI 段階で止められます。コードレビューの負担も減ります。
まとめ
.unwrap()は panic 時の情報量が乏しく、意図がコードに残らない antipattern- 多くの場合
?/expect/unwrap_or/ok_or/let-else/if let/matchのどれかで書ける - 最終手段は「型を絞って
Optionを返す必要をなくす」設計の見直し clippy::unwrap_used = "deny"をCargo.tomlに入れて、 CI で強制する
関連
- clippy
unwrap_used: https://rust-lang.github.io/rust-clippy/master/index.html#unwrap_used std::option::OptionAPI: https://doc.rust-lang.org/std/option/enum.Option.htmlstd::num::NonZero: https://doc.rust-lang.org/std/num/struct.NonZero.html