← Debugging Guides

阅读中文版本

Rust — panicked at called Option::unwrap() on a None value

Debug panics from Option and Result unwrap in Rust, read the panic location and backtrace, replace hot-path unwrap with explicit handling, and use expect with context.

beginnerdebuggingrustpanicserror-handling

Symptoms

  • thread 'main' panicked at called Option::unwrap() on a None value, with a file and line for the unwrap call
  • The panic happens only for some inputs — those where the value is legitimately absent
  • Backtraces (RUST_BACKTRACE=1) point at the unwrap rather than any data corruption

Possible causes

  • A lookup or parse that can fail is unwrapped, and the failing input reaches it in production
  • unwrap used during prototyping survived into shipped code
  • The None case is expected (empty collection, missing optional header) but treated as impossible
  • A chain of unwraps hides which one failed on long lines or nested expressions

Diagnosis steps

Get the exact location and backtrace

RUST_BACKTRACE=1 cargo run -- reproduce-input 2>&1 | head -20

Interpretation

The panic message names file and line; the backtrace shows the call path that produced the None, which is the actual bug context.

Reproduce with the failing input

cargo test failing_case -- --nocapture

Interpretation

A test with the production input that produced None turns the crash into a permanent regression case.

Audit the unwrap inventory

grep -Rn "\.unwrap()\|\.expect(" src/ | grep -v test | head -20

Interpretation

Every unwrap reachable from external input is a panic waiting for its input; this list is the hardening surface.

Root cause

unwrap asserts that the Option is Some (or Result is Ok) and panics otherwise. The panic is not corruption — it is a missing decision about a value that can legitimately be absent. The failing input reached the unwrapped expression in production, where the programmer assumed presence.

Fix

Handle the absent case explicitly: match or if let for Option, ? propagation with a custom error type for Results, expect("specific context") where a genuine invariant justifies the panic, and clippy lint against unwrap in non-test code.

Risk note

Replacing every unwrap with unwrap_or_default can silently fabricate data. Handle absent cases meaningfully or propagate errors.

Verify the fix

The failing production input now produces a typed error or graceful degradation instead of a panic; the unwrap audit shows zero reachable-from-input unwraps.

Symptom walkthrough

The panic message is an address, not just an error:

thread 'main' panicked at 'called `Option::unwrap()` on a `None`
value', src/parser.rs:117:32

src/parser.rs:117:32 is the exact unwrap. Reproduce with the production input and enable the backtrace to see who produced the None:

RUST_BACKTRACE=1 cargo run -- bad-input.csv

The input-dependence is the tell: unit tests pass with well-formed data, and the panic appears the first time an optional field is absent — a normal condition the code treated as impossible.

Fix in practice

Choose by what absence means. Expected absence gets explicit handling:

let locale = headers.get("x-locale")
    .map(|v| v.to_owned())
    .unwrap_or_else(|| "en-US".to_string()); // documented default

Failures propagate as typed errors:

fn parse_count(raw: &str) -> Result<u32, ParseError> {
    raw.parse::<u32>().map_err(|e| ParseError::Count {
        raw: raw.into(), source: e,
    })
}
// callers: ? propagates, no panic anywhere

True invariants may panic, but say why:

let conn = pool.get().expect("pool guarantees a live connection");

And keep the pressure on with tooling — clippy lint or a grep gate in CI that fails non-test unwrap except a reviewed allowlist.