Error Handling with Result
Prerequisites: Option in depth
Rust has no exceptions. A function that can fail returns Result<T, E>:
Ok(value) on success or Err(error) on failure. The type system forces the
caller to deal with the error case before using the value.
enum Result<T, E> { // defined in the standard library
Ok(T),
Err(E),
}
For bugs that should never happen (a broken invariant, an index you already
checked), panic! stops the thread instead. Use Result for failures that are
a normal part of running the program: bad input, a missing file, a network
error.
Syntax
use std::num::ParseIntError;
fn parse_and_double(text: &str) -> Result<i32, ParseIntError> {
let n: i32 = text.parse()?; // on Err, return it from this function immediately
Ok(n * 2)
}
fn main() {
match parse_and_double("21") {
Ok(n) => println!("got {n}"),
Err(e) => println!("error: {e}"),
}
let n = parse_and_double("4").expect("input is a valid number"); // panics with this message on Err
println!("{n}");
}
?works in any function returningResult(orOption). OnErr(e)it returns early, convertingeinto the function's error type withFrom..unwrap()panics onErrwith a generic message..expect("why")panics with your message. Preferexpectin the rare places you do want a panic, and prefer?ormatcheverywhere else.map_errconverts one error type into another, andunwrap_or/unwrap_or_elsesupply a fallback, just like onOption.
Custom error types
A library usually defines one error enum that covers everything that can go
wrong. Implementing Display gives it a human-readable message, and the
std::error::Error trait marks it as a proper error type. Implementing
From<OtherError> lets ? convert errors automatically.
Example
use std::error::Error;
use std::fmt;
use std::num::ParseIntError;
#[derive(Debug)]
enum ConfigError {
MissingValue(String),
BadNumber(ParseIntError),
OutOfRange(u32),
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ConfigError::MissingValue(key) => write!(f, "missing value for '{key}'"),
ConfigError::BadNumber(e) => write!(f, "not a number: {e}"),
ConfigError::OutOfRange(n) => write!(f, "port {n} is out of range"),
}
}
}
impl Error for ConfigError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
ConfigError::BadNumber(e) => Some(e),
_ => None,
}
}
}
// Lets `?` turn a ParseIntError into a ConfigError automatically.
impl From<ParseIntError> for ConfigError {
fn from(e: ParseIntError) -> Self {
ConfigError::BadNumber(e)
}
}
fn parse_port(line: &str) -> Result<u16, ConfigError> {
let (key, value) = line.split_once('=').unwrap_or((line, ""));
if value.is_empty() {
return Err(ConfigError::MissingValue(key.to_string()));
}
let n: u32 = value.parse()?;
if n == 0 || n > 65535 {
return Err(ConfigError::OutOfRange(n));
}
Ok(n as u16)
}
fn main() -> Result<(), Box<dyn Error>> {
for line in ["port=8080", "port=", "port=http", "port=70000"] {
match parse_port(line) {
Ok(port) => println!("{line:12} -> ok, port {port}"),
Err(e) => println!("{line:12} -> error: {e}"),
}
}
// Box<dyn Error> accepts any error type, so `?` works with ours too.
let port = parse_port("port=443")?;
println!("final port: {port}");
Ok(())
}
Expected output
port=8080 -> ok, port 8080
port= -> error: missing value for 'port'
port=http -> error: not a number: invalid digit found in string
port=70000 -> error: port 70000 is out of range
final port: 443
Box<dyn Error> is a convenient error type for main and small programs: any
error type can be converted into it with ?. If main returns an Err, Rust
prints it with Debug formatting and exits with a non-zero status code.
Writing Display, Error and From by hand is repetitive, so two crates are
very common: thiserror derives those impls for your own error enums
(typical in libraries), and anyhow provides a single flexible
anyhow::Error type with context messages (typical in applications).
Common mistakes
- Ignoring a
Result. Calling a function that returns one without using it produceswarning: unused `Result` that must be used. Handle it, propagate it with?, or explicitly discard it withlet _ = ...if that is truly fine. - Using
?when the error types don't match and noFromimpl exists:error[E0277]: `?` couldn't convert the error to `ConfigError`— add aFromimpl or use.map_err(...). - Sprinkling
.unwrap()through code that handles user input. It turns every bad input into a crash. - Returning
Result<T, String>everywhere. It works, but callers can't match on the kind of error. An enum is easier to handle correctly.
Practice
Write fn average(values: &[&str]) -> Result<f64, String> that parses each
string as an f64 and returns their average. Return Err("no values") for an
empty slice and Err("bad value: <text>") if any value fails to parse. Print
the results for ["2", "4", "9"], [] and ["1", "x"].
Show solution
fn average(values: &[&str]) -> Result<f64, String> {
if values.is_empty() {
return Err(String::from("no values"));
}
let mut total = 0.0;
for v in values {
let n: f64 = v.parse().map_err(|_| format!("bad value: {v}"))?;
total += n;
}
Ok(total / values.len() as f64)
}
fn main() {
println!("{:?}", average(&["2", "4", "9"]));
println!("{:?}", average(&[]));
println!("{:?}", average(&["1", "x"]));
}
Ok(5.0)
Err("no values")
Err("bad value: x")