S02: unwrap() / expect() in non-test code
What it looks like
Section titled “What it looks like”use std::fs;use std::path::Path;
pub fn load_port(path: &Path) -> u16 { let text = fs::read_to_string(path).unwrap(); let line = text.lines().find(|l| l.starts_with("port=")).unwrap(); line["port=".len()..].trim().parse().unwrap()}There are three unwrap() calls. Each one panics if its value is an Err or None: a missing file, no port= line, or a port such as 70000 that does not fit in a u16.
Why AI agents produce it
Section titled “Why AI agents produce it”- It is the shortest code that compiles.
fs::read_to_stringreturnsResult<String, io::Error>, andunwrap()turns it into aString. - Examples and tutorials use it. Most short examples online use
unwrap()to stay short, and agents copy the habit. - The happy path works. The code passes a quick manual test with a correct file.
Why it is a problem
Section titled “Why it is a problem”A panic is not an exception that a caller can catch. By default it unwinds the current thread and, in the main thread, ends the program.
- A crash, not an error. A server that panics in a request handler loses that request; a panic in the main thread stops the service. A CLI prints
thread 'main' panicked at src/config.rs:9:44instead of “cannot read /etc/app.conf”. - No context.
called Option::unwrap() on a None valuedoes not say which file or which line was the problem. - The signature lies.
fn load_port(path: &Path) -> u16says that the function always succeeds.
The better version
Section titled “The better version”Return a Result and let the caller decide. ? passes each error up, and map_err adds context:
WorseThree ways to panic. The caller cannot react, and the message has no file name.
use std::fs;use std::path::Path;pub fn load_port(path: &Path) -> u16 {let text = fs::read_to_string(path).unwrap();let line = text.lines().find(|l| l.starts_with("port=")).unwrap();line["port=".len()..].trim().parse().unwrap()}BetterReturns
Result. Each failure has a variant that the caller can match on.use std::num::ParseIntError;use std::path::{Path, PathBuf};use std::{fmt, fs, io};#[derive(Debug)]pub enum ConfigError {Read { path: PathBuf, source: io::Error },MissingPort,BadPort(ParseIntError),}pub fn load_port(path: &Path) -> Result<u16, ConfigError> {let text = fs::read_to_string(path).map_err(|source| ConfigError::Read {path: path.to_path_buf(),source,})?;let value = text.lines().find_map(|l| l.strip_prefix("port=")).ok_or(ConfigError::MissingPort)?;value.trim().parse().map_err(ConfigError::BadPort)}
.ok_or(ConfigError::MissingPort)?turnsNoneinto an error..map_err(ConfigError::BadPort)wraps theParseIntErroras the cause.- In an application (not a library),
anyhow::Resultwith.context("...")is a common, shorter alternative.
See Result, not exceptions and Option, not null.
When it is acceptable
Section titled “When it is acceptable”-
In tests. A panic is how a Rust test fails.
unwrap()in tests is normal. -
For a real invariant, with
expectand a message that says why it cannot fail:/// The text is a constant, so this cannot fail. The message says why.pub fn default_listen_addr() -> SocketAddr {"127.0.0.1:8080".parse().expect("hard-coded address is valid")} -
At program startup, for configuration that the program cannot run without, if the message is clear. Even then, returning an error from
mainusually gives a better message. -
.lock().unwrap()on aMutex. It panics only if another thread panicked while it held the lock. Many teams accept this.
How to detect it
Section titled “How to detect it”- Search for
.unwrap(),.expect(,unwrap_unchecked, andpanic!(outside#[cfg(test)]andtests/. - Clippy
unwrap_usedandexpect_used(restriction group, off by default). Many projects enable them for non-test code:#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))].
What to tell your agent
Section titled “What to tell your agent”In load_port, remove every unwrap(). Return Result<u16, ConfigError>, whereConfigError has variants for a read error (keep the path and the io::Error),a missing port line, and an invalid port (keep the ParseIntError). Use ? topropagate. Only use expect() for real invariants, with a message that says whythe call cannot fail. Add a test for each error variant.Related: S36: panic! used for normal errors and S07: Result<T, String> or stringly-typed errors.