インシデント発生
午前2時。本番ログが悲鳴を上げている。Rustサービスがクラッシュループに陥り、数秒ごとに再起動を繰り返している。stderrを開くと、こんなメッセージが表示される:
thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', src/main.rs:42:25
あるいは、同じくらい厄介な兄弟エラーが:
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: "Connection refused"'
その値は必ず存在すると確信して .unwrap() を呼び出したはずだ。しかし本番環境では、「必ず」という確信はいつか裏切られる。設定ファイルが消え、データベースクエリが空を返し、環境変数が設定されないまま——そしてアプリは、丁寧なエラーメッセージの代わりにスタックトレースを吐いて死ぬ。
なぜこうなるのか
Rustの Option<T> と Result<T, E> 型は、何かが欠けていたり壊れていたりする可能性を、最初から認識するよう強制する。.unwrap() を呼ぶのは、その認識をスキップするショートカットだ。コンパイラにこう告げているのと同じことだ:「この値は必ずある——もしなければ、スレッド全体をクラッシュさせろ」
使い捨てスクリプトやユニットテストなら問題ない。しかしユーザーに届くものに仕込めば、それは地雷だ。
修正手順
1. バックトレースで原因を特定する
パニックが自分のコードではなく、サードパーティクレートの奥深くを指している場合がある。バックトレースを有効にして実行し、完全なコールスタックを取得しよう:
RUST_BACKTRACE=1 cargo run
典型的なWebサービスでは、この出力は30〜50フレームに及ぶことがある。src/ を参照しているフレームを探そう——それが自分のコードだ。それ以外はライブラリの内部なので、今は無視して構わない。
2. 手っ取り早い改善:expect() に切り替える
パニック箇所が起動時であり、クラッシュが正しい結果である場合(重要な設定が欠けているケースなど)は、少なくとも unwrap() を expect() に置き換えよう。動作は同じだが、エラーメッセージがはるかに分かりやすくなる。
// ❌ 失敗時のメッセージが不明瞭
let config = read_config().unwrap();
// ✅ 何が問題だったかを正確に伝える
let config = read_config().expect("Failed to load config.yaml. Is the file present in the working directory?");
適切に書かれた expect メッセージは、次のエンジニアが原因を推測する10分を節約してくれる。
3. パターンマッチングで明示的に処理する
値が欠けていても動き続けるべき処理には、match か if let を使おう。どちらも空のケースのコードを書くことを強制する——それこそがこの型の本質だ。
// match: 両方の分岐を処理する
let user_name = match get_user(id) {
Some(user) => user.name,
None => "Anonymous".to_string(),
};
// if let: 値が存在する場合のみ処理する
if let Some(user) = get_user(id) {
println!("Hello, {}!", user.name);
} else {
println!("User not found.");
}
4. unwrap_or でデフォルト値を提供する
適切なフォールバックがある場合は、matchを使わずに済む。
// 静的なデフォルト値
let port = std::env::var("PORT").unwrap_or_else(|_| "8080".to_string());
// 遅延デフォルト — 値が欠けている場合のみクロージャが実行される
let data = fetch_cached_data().unwrap_or_else(|| fetch_from_api());
フォールバックの計算コストが高い場合は unwrap_or_else(クロージャ付き)を使おう——必要な場合にしか実行されない。
5. ? 演算子でエラーを伝播させる
多くの場合、None や Err に遭遇した関数がその対処を決めるべきではない——それは呼び出し元の仕事だ。? 演算子はエラーを自動的に返して早期リターンする:
fn get_api_key() -> Result<String, ConfigError> {
// var() が Err を返した場合、ここで関数を抜けてその Err を返す
let key = std::env::var("API_KEY")?;
Ok(key)
}
これを複数チェーンすれば、ハッピーパスはすっきりしたままになる。エラーは実際に対処できる層——通常は失敗をログに記録して適切なHTTP 500またはexitコードで応答するトップレベルのハンドラ——まで自然に伝播する。
検証
修正完了と判断する前に、意図的に障害を再現しよう:
- `None` や `Err` を引き起こした環境変数を削除するか、ファイルを消す。
- アプリケーションを起動する。
- 正常に回復すること(警告をログに記録し、フォールバックを使用する)、またはクリーンなエラーメッセージで終了すること——パニックなし、stderrへのスタックトレース出力なし——を確認する。
本番環境向けのヒント
- **unwrapをビルドエラーにする:** `main.rs` または `lib.rs` の先頭に `#![deny(clippy::unwrap_used)]` を追加しよう。裸の `unwrap()` が紛れ込んだPRはCIで弾かれる。
- **アプリにはanyhow、ライブラリにはthiserror:** `anyhow` クレートを使えば、単一の `?` で任意のエラー型をラップし、`.context("何をしていたか")` でコンテキストを付加できる。クレートを公開していて、呼び出し元が特定のエラーバリアントをマッチする必要がある場合は `thiserror` を使おう。
- **処理済みエラーをログに記録する:** `None` や `Err` をキャッチして処理を続行する場合は、`tracing::warn!` または `log::warn!` でログに残そう。サイレントなフォールバックは本物の問題を隠してしまう——今日の警告が明日のインシデントになる。

