じゃあ、おうちで学べる

本能を呼び覚ますこのコードに、君は抗えるか

`unwrap()` を書きそうになったときの選択肢8つ

はじめに

Rust で OptionResult から値を取り出すとき、つい .unwrap() を書いてしまう場面があります。動くし短い。だから書く。だけどそのコードは、エラー時にスタックトレースなしで panic します。本番で踏むと 「どこで何の理由で死んだのか」 がわからない事故になります。

この記事は、.unwrap() を書きたくなった瞬間の代替案を8個並べます。状況に応じて選んでください。

前提: なぜ unwrap() は 良くないか

.unwrap() の問題は3つです。

  1. panic メッセージが「called \Option::unwrap()` on a `None` value`」のような汎用文言で、原因の手がかりがない
  2. どんな理由で None / Err が起きたかドキュメントに残らない
  3. 後で読み返したときに「ここは絶対 None にならないと信じている」のか「面倒で書いた」のか区別がつかない

Cargo.tomlclippy::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_elseOptionResult に変える

Option<T>? で伝搬したい」場面。ok_orResult<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() を書きそうになったとき、上から順にチェックします。

  1. Result を返す関数なら → ?
  2. 絶対あるはずだが panic で気付きたい → .expect("理由")
  3. None / Err なら fallback でよい → unwrap_or / unwrap_or_else
  4. OptionResult に変えたい → ok_or / ok_or_else
  5. 失敗時は早期 return → let-else
  6. 複数 Option を AND で扱う → if let chain
  7. エラー variant ごとに分岐したい → match
  8. 「そもそも Option を返す必要があるか」 → 型を絞る

Cargo.tomlunwrap_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 で強制する

関連