Learn › Rust Programming

Error Handling

Learn idiomatic error handling in Rust with Result, Option, the ? operator, and custom errors.

The Result Type

Rust uses the Result enum for functions that can fail in a recoverable way. Result has two variants: Ok which holds the success value and Err which holds the error value. Unlike exceptions in other languages, errors in Rust are values that must be explicitly handled. The compiler will warn you if you ignore a Result, ensuring that error conditions are always addressed. This makes Rust programs more robust and easier to debug.

use std::num::ParseIntError;

fn parse_and_double(s: &str) -> Result<i32, ParseIntError> {
    let number = s.parse::<i32>()?;
    Ok(number * 2)
}

fn main() {
    let inputs = vec!["5", "10", "abc", "42", ""];

    for input in inputs {
        match parse_and_double(input) {
            Ok(result) => {
                println!("'{}' -> {}", input, result)
            }
            Err(e) => {
                println!("'{}' -> Error: {}", input, e)
            }
        }
    }

    // Using unwrap_or_else for error recovery
    let value = "not_a_number"
        .parse::<i32>()
        .unwrap_or_else(|_| {
            println!("Parse failed, using default");
            0
        });
    println!("Value: {}", value);
}

The ? Operator

The question mark operator provides a concise way to propagate errors up the call stack. When used on a Result, it either unwraps the Ok value and continues execution or returns the Err value from the current function immediately. This eliminates verbose match expressions for error handling and lets you write clean, linear code. The ? operator also works with Option, returning None from the function if the value is None.

use std::fs;
use std::io;

fn read_username_from_file() -> Result<String, io::Error> {
    let mut content = fs::read_to_string("username.txt")?;
    content = content.trim().to_string();
    Ok(content)
}

fn first_even(numbers: &[i32]) -> Option<i32> {
    let first = numbers.first()?;
    if first % 2 == 0 {
        Some(*first)
    } else {
        None
    }
}

fn chain_operations(input: &str) -> Result<i32, String> {
    let trimmed = input.trim();
    if trimmed.is_empty() {
        return Err("Input is empty".to_string());
    }
    let number: i32 = trimmed
        .parse()
        .map_err(|e| format!("Parse error: {}", e))?;
    if number < 0 {
        return Err("Number must be non-negative".to_string());
    }
    Ok(number * number)
}

fn main() {
    match read_username_from_file() {
        Ok(name) => println!("Username: {}", name),
        Err(e) => println!("Could not read username: {}", e),
    }

    println!("First even of [4,5,6]: {:?}", first_even(&[4, 5, 6]));
    println!("First even of [3,5,7]: {:?}", first_even(&[3, 5, 7]));
    println!("First even of []: {:?}", first_even(&[]));

    for input in ["  49  ", "", "hello", "-5", "16"] {
        println!("chain('{}') = {:?}", input, chain_operations(input));
    }
}

Custom Error Types

For larger applications you will want to define your own error types that can represent the various failure modes of your program. A common approach is to create an enum where each variant represents a different kind of error. Implementing the Display and Error traits on your error type lets it integrate with the rest of the Rust error ecosystem. The From trait can be implemented to enable automatic conversion from other error types when using the ? operator.

use std::fmt;
use std::num::ParseIntError;

#[derive(Debug)]
enum AppError {
    InvalidInput(String),
    ParseFailure(ParseIntError),
    OutOfRange { value: i32, min: i32, max: i32 },
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppError::InvalidInput(msg) => {
                write!(f, "Invalid input: {}", msg)
            }
            AppError::ParseFailure(e) => {
                write!(f, "Parse failure: {}", e)
            }
            AppError::OutOfRange { value, min, max } => {
                write!(
                    f,
                    "{} is out of range [{}, {}]",
                    value, min, max
                )
            }
        }
    }
}

impl From<ParseIntError> for AppError {
    fn from(e: ParseIntError) -> Self {
        AppError::ParseFailure(e)
    }
}

fn validate_age(input: &str) -> Result<i32, AppError> {
    if input.trim().is_empty() {
        return Err(AppError::InvalidInput(
            "age cannot be empty".to_string(),
        ));
    }
    let age: i32 = input.trim().parse()?; // auto-converts
    if age < 0 || age > 150 {
        return Err(AppError::OutOfRange {
            value: age,
            min: 0,
            max: 150,
        });
    }
    Ok(age)
}

fn main() {
    for input in ["25", "", "abc", "200", "0"] {
        match validate_age(input) {
            Ok(age) => println!("Valid age: {}", age),
            Err(e) => println!("Error: {}", e),
        }
    }
}

← Pattern Matching · Collections →