Skip to content

S02: unwrap() / expect() in non-test code

S02Severity: High

The program stops (panics) on a failure that the caller could handle.

Clippy: unwrap_used, expect_used

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.

  • It is the shortest code that compiles. fs::read_to_string returns Result<String, io::Error>, and unwrap() turns it into a String.
  • 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.

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:44 instead of “cannot read /etc/app.conf”.
  • No context. called Option::unwrap() on a None value does not say which file or which line was the problem.
  • The signature lies. fn load_port(path: &Path) -> u16 says that the function always succeeds.

Return a Result and let the caller decide. ? passes each error up, and map_err adds context:

  1. 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()
    }
  2. 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)? turns None into an error.
  • .map_err(ConfigError::BadPort) wraps the ParseIntError as the cause.
  • In an application (not a library), anyhow::Result with .context("...") is a common, shorter alternative.

See Result, not exceptions and Option, not null.

  • In tests. A panic is how a Rust test fails. unwrap() in tests is normal.

  • For a real invariant, with expect and 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 main usually gives a better message.

  • .lock().unwrap() on a Mutex. It panics only if another thread panicked while it held the lock. Many teams accept this.

  • Search for .unwrap(), .expect(, unwrap_unchecked, and panic!( outside #[cfg(test)] and tests/.
  • Clippy unwrap_used and expect_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))].
In load_port, remove every unwrap(). Return Result<u16, ConfigError>, where
ConfigError 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 ? to
propagate. Only use expect() for real invariants, with a message that says why
the 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.