Syntax Cache
BlogMethodFeaturesHow It WorksBuild a Game
  1. Home
  2. Cheat Sheets
  3. Rust
  4. Rust Error Handling Cheat Sheet
RustCheat Sheet

Rust Error Handling Cheat Sheet

Quick-reference for Result, Option, the ? operator, and error propagation patterns. Each section includes copy-ready snippets with inline output comments.

On this page
  1. 1Result<T, E>
  2. 2Option<T>
  3. 3The ? Operator
  4. 4unwrap and expect
  5. 5Result Combinators
  6. 6Option Combinators
  7. 7Converting Error Types
  8. 8Custom Error Types
  9. 9anyhow for Applications
  10. 10panic! vs Result
  11. 11Collecting Results
Result<T, E>Option<T>The ? Operatorunwrap and expectResult CombinatorsOption CombinatorsConverting Error TypesCustom Error Typesanyhow for Applicationspanic! vs ResultCollecting Results

Result<T, E>

Result is an enum with two variants: Ok(T) for success and Err(E) for failure. Rust has no exceptions -- Result is how you handle errors.

Creating Result values
let ok: Result<i32, String> = Ok(42);
let err: Result<i32, String> = Err("something went wrong".into());
Match on Result
match "42".parse::<i32>() {
    Ok(n) => println!("parsed: {n}"),
    Err(e) => println!("error: {e}"),
}
Returning Result from a function
fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err("division by zero".into())
    } else {
        Ok(a / b)
    }
}

Option<T>

Option represents a value that might be absent: Some(T) or None. Rust has no null -- Option replaces it.

Creating Option values
let some: Option<i32> = Some(5);
let none: Option<i32> = None;
Common Option sources
let v = vec![10, 20, 30];
v.first()              // => Some(&10)
v.get(99)              // => None
"hello".find('l')      // => Some(2)
"hello".find('z')      // => None
Match on Option
match v.first() {
    Some(val) => println!("first: {val}"),
    None => println!("empty"),
}

The ? Operator

The ? operator unwraps Ok/Some or returns early with Err/None. The function must return a compatible type.

? with Result: propagate errors
use std::fs;

fn read_config() -> Result<String, std::io::Error> {
    let content = fs::read_to_string("config.toml")?;  // returns Err on failure
    Ok(content.trim().to_string())
}
? with Option: propagate None
fn first_word(s: &str) -> Option<&str> {
    let end = s.find(' ')?;  // returns None if no space
    Some(&s[..end])
}
Chaining multiple ? calls
fn load_port() -> Result<u16, Box<dyn std::error::Error>> {
    let text = std::fs::read_to_string("port.txt")?;
    let port = text.trim().parse::<u16>()?;
    Ok(port)
}

Box<dyn Error> accepts any error type, making ? work across different error types.

? in main()
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = std::fs::read_to_string("config.toml")?;
    println!("{config}");
    Ok(())
}

unwrap and expect

Both panic on Err/None. Use expect to document the assumption; use ? in production code.

unwrap: panic with default message
let n: i32 = "42".parse().unwrap();      // => 42
// "abc".parse::<i32>().unwrap();          // PANIC
expect: panic with custom message
let port: u16 = env::var("PORT")
    .expect("PORT must be set")
    .parse()
    .expect("PORT must be a valid u16");

The message should explain the assumption, not the symptom.

When each is appropriate
// Tests: unwrap is fine -- panic = test failure
#[test]
fn test_parse() {
    let result = parse("valid").unwrap();
    assert_eq!(result, 42);
}

// Provably safe: expect documents why
let one: u32 = "1".parse().expect("literal '1' is always valid");

// Production: prefer ? to propagate
fn load() -> Result<Config, Error> {
    let text = std::fs::read_to_string("config.toml")?;
    Ok(parse_config(&text)?)
}

Result Combinators

Transform Ok/Err values without match. Chain combinators for clean error handling pipelines.

map: transform the Ok value
let result: Result<i32, String> = Ok(5);
let doubled = result.map(|x| x * 2);  // => Ok(10)
map_err: transform the Err value
let result = std::fs::read_to_string("file.txt")
    .map_err(|e| format!("failed to read: {e}"))?;
and_then: chain fallible operations
fn parse_port(s: &str) -> Result<u16, String> {
    s.trim()
        .parse::<u16>()
        .map_err(|e| format!("bad port: {e}"))
}

let port = std::fs::read_to_string("port.txt")
    .map_err(|e| format!("read failed: {e}"))
    .and_then(|s| parse_port(&s));
unwrap_or and unwrap_or_else
let port = env::var("PORT")
    .ok()
    .and_then(|v| v.parse().ok())
    .unwrap_or(8080);  // default if any step fails

// unwrap_or_else: lazy default (only computed on Err)
let config = load_config()
    .unwrap_or_else(|e| {
        eprintln!("warn: {e}");
        Config::default()
    });

Option Combinators

Transform Some/None values. Convert between Option and Result with ok_or.

map: transform the inner value
let opt = Some("hello");
let upper = opt.map(|s| s.to_uppercase());  // => Some("HELLO")
and_then (flatmap): chain Options
let opt = Some("42");
let parsed = opt.and_then(|s| s.parse::<i32>().ok());  // => Some(42)
ok_or: Option to Result
let name: Option<&str> = Some("Alice");
let result: Result<&str, &str> = name.ok_or("name required");

// ok_or_else: lazy error (only created on None)
let result = name.ok_or_else(|| format!("missing at line {}", n));

Use ok_or for cheap static errors. Use ok_or_else when the error involves allocation.

unwrap_or: default on None
let timeout = config.get("timeout")
    .and_then(|v| v.parse::<u64>().ok())
    .unwrap_or(30);  // default 30
filter: keep Some only if predicate passes
let even = Some(4).filter(|&x| x % 2 == 0);  // => Some(4)
let odd = Some(3).filter(|&x| x % 2 == 0);   // => None

Converting Error Types

The ? operator auto-converts errors via the From trait. Use map_err for manual conversion.

map_err for inline conversion
fn load_port() -> Result<u16, String> {
    let text = std::fs::read_to_string("port.txt")
        .map_err(|e| format!("read failed: {e}"))?;
    let port = text.trim().parse::<u16>()
        .map_err(|e| format!("bad port: {e}"))?;
    Ok(port)
}
From trait for automatic conversion
// If AppError implements From<io::Error>,
// then ? converts automatically:
fn load() -> Result<String, AppError> {
    let s = std::fs::read_to_string("file.txt")?;  // io::Error -> AppError
    Ok(s)
}
Box<dyn Error> accepts everything
use std::error::Error;

fn run() -> Result<(), Box<dyn Error>> {
    let text = std::fs::read_to_string("data.txt")?;   // io::Error
    let num: i32 = text.trim().parse()?;                // ParseIntError
    println!("{num}");
    Ok(())
}

Box<dyn Error> is great for applications. For libraries, use concrete error types so callers can match.

Custom Error Types

Define an error enum with From impls so ? auto-converts. Use thiserror to reduce boilerplate.

Manual error enum
use std::fmt;

#[derive(Debug)]
enum AppError {
    Io(std::io::Error),
    Parse(std::num::ParseIntError),
    Config(String),
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppError::Io(e) => write!(f, "IO: {e}"),
            AppError::Parse(e) => write!(f, "parse: {e}"),
            AppError::Config(msg) => write!(f, "config: {msg}"),
        }
    }
}

impl From<std::io::Error> for AppError {
    fn from(e: std::io::Error) -> Self { AppError::Io(e) }
}
thiserror: derive Display and From
use thiserror::Error;

#[derive(Debug, Error)]
enum AppError {
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),

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

    #[error("config error: {0}")]
    Config(String),
}

thiserror generates Display and From impls from attributes. Use for library error types.

anyhow for Applications

The anyhow crate provides ergonomic error handling for applications (not libraries).

anyhow::Result and context
use anyhow::{Context, Result};

fn load_config() -> Result<Config> {
    let text = std::fs::read_to_string("config.toml")
        .context("failed to read config file")?;

    let config: Config = toml::from_str(&text)
        .context("failed to parse config")?;

    Ok(config)
}
anyhow::bail! and ensure!
use anyhow::{bail, ensure, Result};

fn validate(port: u16) -> Result<()> {
    ensure!(port > 0, "port must be positive");

    if port > 65535 {
        bail!("port {port} out of range");
    }

    Ok(())
}

bail! returns Err immediately. ensure! is like assert but returns Err instead of panicking.

panic! vs Result

panic! is for bugs and impossible states. Result is for expected failures the caller can handle.

When to panic
// Impossible state: logic error, not user input
fn index_twice(slice: &[i32], i: usize, j: usize) -> (i32, i32) {
    assert!(i != j, "indices must differ");
    (slice[i], slice[j])
}

// Violated contract in unsafe code
// Array out of bounds (bug, not user error)
When to return Err
// User input might be invalid
fn parse_age(input: &str) -> Result<u32, String> {
    input.parse::<u32>()
        .map_err(|_| format!("invalid age: {input}"))
}

// File might not exist
fn read_config() -> Result<String, std::io::Error> {
    std::fs::read_to_string("config.toml")
}
Rule of thumb
// panic!  => bug in your code (should never happen)
// Result  => expected failure (bad input, missing file, network error)
// Option  => value might be absent (not an error, just missing)

Collecting Results

collect() can turn an iterator of Results into a Result of a collection. It short-circuits on the first Err.

Collect into Result<Vec<T>, E>
let strings = vec!["1", "2", "three", "4"];
let numbers: Result<Vec<i32>, _> = strings
    .iter()
    .map(|s| s.parse::<i32>())
    .collect();
// => Err(ParseIntError)  -- stops at "three"
Skip errors with filter_map
let numbers: Vec<i32> = strings
    .iter()
    .filter_map(|s| s.parse::<i32>().ok())
    .collect();
// => [1, 2, 4]  -- "three" silently skipped
Partition successes and failures
let (oks, errs): (Vec<_>, Vec<_>) = strings
    .iter()
    .map(|s| s.parse::<i32>())
    .partition(Result::is_ok);

let numbers: Vec<i32> = oks.into_iter().map(Result::unwrap).collect();
let errors: Vec<_> = errs.into_iter().map(Result::unwrap_err).collect();
Learn Rust in Depth
Rust Structs & Enums Practice →Rust Pattern Matching Practice →
Warm-up1 / 2

Can you write this from memory?

Write a function signature for parse_number that takes a &str and returns Result<i32, String>.

See Also
Pattern Matching →Traits & Generics →

Start Practicing Rust

Free daily exercises with spaced repetition. No credit card required.

← Back to Rust Syntax Practice
Syntax Cache

Build syntax muscle memory with spaced repetition.

Product

  • Pricing
  • Our Method
  • Daily Practice
  • Design Patterns
  • Interview Prep

Resources

  • Blog
  • Compare
  • Cheat Sheets
  • Vibe Coding
  • Muscle Memory

Languages

  • Python
  • JavaScript
  • TypeScript
  • Rust
  • SQL
  • GDScript

Legal

  • Terms
  • Privacy
  • Contact

© 2026 Syntax Cache

Cancel anytime in 2 clicks. Keep access until the end of your billing period.

No refunds for partial billing periods.