Skip to content
AgileLogics
6 min read

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 returning Result (or Option). On Err(e) it returns early, converting e into the function's error type with From.
  • .unwrap() panics on Err with a generic message. .expect("why") panics with your message. Prefer expect in the rare places you do want a panic, and prefer ? or match everywhere else.
  • map_err converts one error type into another, and unwrap_or / unwrap_or_else supply a fallback, just like on Option.

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 produces warning: unused `Result` that must be used. Handle it, propagate it with ?, or explicitly discard it with let _ = ... if that is truly fine.
  • Using ? when the error types don't match and no From impl exists: error[E0277]: `?` couldn't convert the error to `ConfigError` — add a From impl 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")