5 min read #rust #Error Handling
On this page

Result and Option

Option represents "a value might not exist", Result represents "an operation might fail"—both are enums, not exceptions. The ? operator returns early when encountering None/Err, and combinator chains (map/and_then/or_else) turn error handling into a type-safe pipeline rather than control-flow jumps via try-catch.

Option: A Value Might Not Exist

fn find_user(id: u64) -> Option<User> { ... }

find_user(42)
    .map(|u| u.name)                 // Option<User> → Option<String>, transform
    .and_then(|name| db_lookup(&name)) // Option<String> → Option<Record>, chain
    .filter(|r| r.active)            // keep only if active
    .unwrap_or(default_record);      // extract or use default

Each combinator only handles the Some case—None short-circuits automatically. This pattern is known in functional programming as "railway oriented programming": either you stay on the success track, or you instantly jump to the failure track and stay there, checking for nulls nowhere along the way. The compiler forces you to provide a default at unwrap_or—you can't forget to handle missing values.

Result: Errors Are Values, Not Exceptions

fn parse(s: &str) -> Result<i32, ParseIntError> { s.parse() }

let n = match parse("42") {
    Ok(n) => n * 2,
    Err(e) => { eprintln!("parse failed: {}", e); return; }
};

The core difference from exceptions: errors are part of the return value, so callers know from the function signature that it might fail. There is no hidden control flow, no need to remember "which exceptions this function throws." The compiler forces you to handle Result—the #[must_use] attribute generates a compiler warning for unused Results.

? Operator: Early Return on Errors

fn read_config(path: &str) -> Result<Config, Error> {
    let file = std::fs::read_to_string(path)?;    // Err → early return
    let config: Config = toml::from_str(&file)?;
    Ok(config)
}

Expanded equivalent:

let file = match std::fs::read_to_string(path) {
    Ok(v) => v,
    Err(e) => return Err(e.into()),  // .into() converts error types via the From trait
};

The ? operator calls From::from to perform automatic error type conversion—as long as From<io::Error> for MyError is implemented, the io::Error from std::fs::read_to_string()? will automatically convert to MyError. This is key to Rust's error propagation—you don't need to manually wrap errors at every layer.

main Returns Result

fn main() -> Result<(), Box<dyn Error>> {
    let config = read_config()?;     // ? in main!
    Ok(())
}

When main returns a Result, if main returns Err, the runtime prints the error and exits (exit code 1). This is suitable for simple CLI tools. Complex applications typically have their own error handling and logging.

References

  • Rust Book: Chapter 9
  • Rust By Example: Option/Result, ?

Keywords: Option, Result, combinator, and_then, ?, error propagation, From trait, #[must_use]