Rust Error Handling Cheat Sheet β€” Result, Option, ? Operator, unwrap | Dataplexa

Rust Error Handling

Result<T,E>  Β·  Option chaining  Β·  the ? operator  Β·  unwrap vs expect  Β·  custom errors  Β·  panic vs recoverable

Sheet 2 of 2 Rust 2021 Intermediate Printable

Two Error Strategies

unrecoverable Β· recoverable
panic! β€” Unrecoverable
// Immediately stops the thread
panic!("index out of bounds");

// Triggers automatically on bad access
let v = vec![1, 2, 3];
v[99];   // panics at runtime

// Set RUST_BACKTRACE=1 to see trace
// $ RUST_BACKTRACE=1 cargo run

// Use panic for:
// - bugs / programming errors
// - tests
// - prototype code
// - truly unrecoverable states
Result<T, E> β€” Recoverable
// The Result enum (in std prelude)
enum Result<T, E> {
    Ok(T),    // success value
    Err(E),   // error value
}

// Functions that can fail return Result
use std::fs::File;

let f = File::open("hello.txt");
// f is Result<File, std::io::Error>

// Use for:
// - I/O operations
// - parsing / conversion
// - network calls
// - any fallible operation
Decision β€” which to use?
// Use panic! when:
// βœ— Invalid input that CAN'T happen
// βœ— Tests β€” unwrap() is fine
// βœ— Prototype / example code
// βœ— State is fundamentally broken

// Use Result when:
// βœ“ File might not exist
// βœ“ Network could fail
// βœ“ User input might be invalid
// βœ“ Caller should decide what to do

// Golden rule:
// Libraries β†’ always Result
// Binaries β†’ can panic on bad state
Rust has no exceptions. Errors are just values β€” Result<T, E> for recoverable errors, panic! for unrecoverable ones. The type system forces you to acknowledge every possible error at compile time.

Result<T, E> β€” Core Usage

match Β· if let
Handle with match
use std::fs::File;
use std::io::ErrorKind;

let f = match File::open("log.txt") {
    Ok(file)   => file,
    Err(e)     => match e.kind() {
        // Create if not found
        ErrorKind::NotFound =>
            match File::create("log.txt") {
                Ok(fc) => fc,
                Err(e) => panic!("{e}"),
            },
        other => panic!("{other:?}"),
    },
};
Returning a Result from a function
use std::io::{self, Read};
use std::fs::File;

fn read_username() -> Result<String, io::Error> {
    let mut f = match File::open("user.txt") {
        Ok(f)  => f,
        Err(e) => return Err(e),
    };
    let mut s = String::new();
    match f.read_to_string(&mut s) {
        Ok(_)  => Ok(s),
        Err(e) => Err(e),
    }
}

The ? Operator

propagate errors cleanly
? on Result β€” early return on Err
// ? unwraps Ok, or returns Err early
// Same function as before β€” much cleaner:
fn read_username() -> Result<String, io::Error> {
    let mut f = File::open("user.txt")?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}

// Chain ? calls together
fn read_username() -> Result<String, io::Error> {
    let mut s = String::new();
    File::open("user.txt")?
        .read_to_string(&mut s)?;
    Ok(s)
}
? on Option & in main()
// ? also works on Option β€” returns None
fn first_char(s: &str) -> Option<char> {
    let line = s.lines().next()?;   // Option?
    let ch   = line.chars().next()?; // Option?
    Some(ch)
}

// Use ? in main() by returning Result
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let f = File::open("config.txt")?;
    // ... do work ...
    Ok(())
}
? also converts error types. It calls From::from(e) on the error, so if your function returns a different error type, Rust will auto-convert it β€” as long as a From impl exists.

unwrap Β· expect Β· unwrap_or Β· unwrap_or_else

extracting values safely
unwrap & expect
// unwrap β€” panics with generic message
let f = File::open("hi.txt").unwrap();
// panics: "called Result::unwrap() on Err"

// expect β€” panics with YOUR message
let f = File::open("hi.txt")
    .expect("hi.txt should exist");
// panics: "hi.txt should exist: ..."

// Use unwrap/expect only in:
// - tests
// - prototypes
// - cases you're 100% certain succeed
Safe alternatives β€” no panic
let res: Result<i32, &str> = Err("fail");

// Default value
res.unwrap_or(0)              // 0

// Compute default lazily
res.unwrap_or_else(|e| {
    eprintln!("Error: {e}");
    -1
})

// Default from Default trait
res.unwrap_or_default()      // 0 (i32::default)

// Check then get
if res.is_ok() {
    let val = res.unwrap();  // safe here
}
Result combinators
let ok: Result<i32, &str> = Ok(2);

// map β€” transform Ok value
ok.map(|n| n * 3)          // Ok(6)

// map_err β€” transform Err value
ok.map_err(|e| format!("{e}"))

// and_then β€” chain fallible ops
ok.and_then(|n|
    if n > 0 { Ok(n) }
    else { Err("neg") }
)

// or β€” use fallback Result
let err: Result<i32, &str> = Err("x");
err.or(Ok(5))              // Ok(5)
Never use unwrap() in library code. Callers can't handle a panic β€” always return Result from library functions so downstream code can decide how to respond.

Option<T> β€” Full Reference

chaining Β· combinators
Safe extraction
let opt: Option<i32> = Some(42);

opt.unwrap_or(0)         // 42
opt.unwrap_or_default()  // 42
opt.unwrap_or_else(|| compute())

opt.is_some()            // true
opt.is_none()            // false

// Convert Option β†’ Result
opt.ok_or("was None")
// Ok(42)

let none: Option<i32> = None;
none.ok_or("was None")
// Err("was None")
Combinators
let s: Option<String> =
    Some(String::from("hello"));

// map β€” transform if Some
s.map(|v| v.len())    // Some(5)

// and_then β€” chain Option ops
s.as_deref()
 .and_then(|v|
    if v.len() > 3 { Some(v) }
    else { None }
)                      // Some("hello")

// filter β€” keep if predicate true
Some(4).filter(|n| n % 2 == 0) // Some(4)
Some(3).filter(|n| n % 2 == 0) // None

// flatten β€” Option<Option<T>> β†’ Option<T>
Some(Some(5)).flatten()  // Some(5)

Custom Error Types

std::error::Error Β· From Β· Display
Implement std::error::Error
use std::fmt;

// 1. Define your error type
#[derive(Debug)]
enum AppError {
    NotFound(String),
    ParseError(std::num::ParseIntError),
    IoError(std::io::Error),
}

// 2. Implement Display
impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter) ->
        fmt::Result
    {
        match self {
            AppError::NotFound(s) =>
                write!(f, "not found: {s}"),
            AppError::ParseError(e) =>
                write!(f, "parse error: {e}"),
            AppError::IoError(e) =>
                write!(f, "io error: {e}"),
        }
    }
}

// 3. Impl Error (can be empty)
impl std::error::Error for AppError {}
From β€” auto conversion with ?
// Implement From so ? auto-converts
impl From<std::io::Error>
    for AppError
{
    fn from(e: std::io::Error) -> Self {
        AppError::IoError(e)
    }
}

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

// Now ? works across error types!
fn run() -> Result<(), AppError> {
    let _f = File::open("x.txt")?;   // IoError
    let _n = "abc".parse::<i32>()?; // ParseError
    Ok(())
}
Box<dyn Error> β€” quick & easy
// No custom type β€” good for binaries
type BoxError =
    Box<dyn std::error::Error>;

fn run() -> Result<(), BoxError> {
    let _f = File::open("x.txt")?;
    let _n = "abc".parse::<i32>()?;
    Ok(())
}

// type alias for cleaner signatures
type Result<T> =
    std::result::Result<T, BoxError>;

fn process() -> Result<String> {
    Ok(String::from("done"))
}
thiserror & anyhow crates make this much easier. thiserror generates boilerplate for library errors via #[derive(Error)]. anyhow gives you anyhow::Result<T> with context chaining β€” ideal for applications.

thiserror crate

library errors
Cargo.toml
# Cargo.toml
[dependencies]
thiserror = "1"
Define errors with #[derive(Error)]
use thiserror::Error;

#[derive(Error, Debug)]
enum AppError {
    #[error("file not found: {0}")]
    NotFound(String),

    #[error("io error")]
    Io(#[from] std::io::Error),

    #[error("parse failed")]
    Parse(#[from] std::num::ParseIntError),
}

// Generates Display, Error, and From impls
// automatically β€” no boilerplate!
fn run() -> Result<(), AppError> {
    File::open("x.txt")?;  // auto From
    Ok(())
}

anyhow crate

application errors
Cargo.toml
# Cargo.toml
[dependencies]
anyhow = "1"
Context & bail!
use anyhow::{Context, Result, bail, ensure};

fn run() -> Result<()> {
    // ? works on any error type
    let _f = File::open("x.txt")
        .context("opening config file")?;

    // bail! β€” early return with error
    let n: i32 = -1;
    if n < 0 { bail!("n must be positive, got {n}"); }

    // ensure! β€” assert with error
    ensure!(n > 0, "n ({n}) must be positive");

    Ok(())
}
Use thiserror for libraries, anyhow for apps. Libraries need typed errors so callers can match on them. Applications just need to display errors to the user β€” anyhow's context chaining gives great diagnostics.

Error Propagation Patterns

real-world recipes
Collect Results from iterators
// Parse a list of strings to ints
let strings = vec!["1","2","3"];

// Fail on first error
let nums: Result<Vec<i32>, _> =
    strings.iter()
    .map(|s| s.parse::<i32>())
    .collect();
// Ok([1, 2, 3])

// Collect only successes (ignore errors)
let ok_only: Vec<i32> =
    vec!["1","x","3"]
    .iter()
    .filter_map(|s| s.parse().ok())
    .collect();
// [1, 3]
Multiple error types with map_err
use std::num::ParseIntError;

#[derive(Debug)]
enum MyErr { Parse(ParseIntError), Neg }

fn parse_pos(s: &str) -> Result<u32, MyErr> {
    // Convert ParseIntError β†’ MyErr::Parse
    let n: i32 = s.parse()
        .map_err(MyErr::Parse)?;

    // Check sign
    if n < 0 { return Err(MyErr::Neg); }
    Ok(n as u32)
}
Error context with anyhow
use anyhow::{Context, Result};

fn load_config(path: &str) -> Result<String> {
    let mut f = File::open(path)
        // Add context to any error
        .with_context(||
            format!("opening config: {path}"))?;
    let mut s = String::new();
    f.read_to_string(&mut s)
        .context("reading config file")?;
    Ok(s)
}
// Error chain: "reading config file:
//   opening config: conf.toml:
//   No such file or directory (os error 2)"

Error Handling β€” Quick Reference

all methods at a glance
Method / Operator Works on Returns Behaviour
? Result / Option T (unwrapped) Returns Err/None early, propagates to caller
unwrap() Result / Option T Panics with generic message if Err/None
expect("msg") Result / Option T Panics with your message if Err/None
unwrap_or(default) Result / Option T Returns value or the given default
unwrap_or_else(fn) Result / Option T Returns value or calls closure for default
unwrap_or_default() Result / Option T Returns value or T::default()
map(fn) Result / Option Result<U,E> / Option<U> Transforms Ok/Some value, passes Err/None through
map_err(fn) Result Result<T,F> Transforms Err value, passes Ok through
and_then(fn) Result / Option Result<U,E> / Option<U> Chains another fallible operation if Ok/Some
or(res) Result / Option Result / Option Returns self if Ok/Some, otherwise the argument
ok() Result Option<T> Converts Ok(v) β†’ Some(v), Err β†’ None
ok_or(err) Option Result<T,E> Converts Some(v) β†’ Ok(v), None β†’ Err(err)
is_ok() / is_err() Result bool Check without consuming
is_some() / is_none()Option bool Check without consuming
filter_map(fn) Iterator Iterator<T> map + flatten β€” keeps only Some results

Error Handling Mastery Checklist

sheet 2 complete
Result FundamentalsKey point
Know when to panic vs use Resultbugs vs expected failures
Handle Result with matchOk(v) / Err(e) arms
Return Result from functionspropagate to caller
Use ? to propagate cleanlyearly return on Err
Safe ExtractionKey point
Prefer unwrap_or over unwrapno panic risk
Use expect with a clear messagetests / truly impossible
Chain combinators: map, and_thentransform without unwrapping
Convert between Option and Resultok() / ok_or()
Custom ErrorsKey point
Impl Display + Error for custom typerequired for std compat
Impl From for auto ? conversion? calls From::from(e)
Use thiserror in libraries#[derive(Error)]
Use anyhow in applications.context() chains
Rust series complete!  Β·  You've covered ownership, borrowing, lifetimes, enums, pattern matching, and the full error handling system. Next steps: explore closures & iterators, async/await, or smart pointers (Box, Rc, Arc, RefCell).