Rust Cheatsheet

Error Handling

Use this Rust reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

Result\<T, E\> Basics

The primary error type in Rust. Functions that can fail return Result<T, E>.

fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err("division by zero".to_string())
    } else {
        Ok(a / b)
    }
}

match divide(10.0, 2.0) {
    Ok(result) => println!("Result: {}", result),
    Err(e) => println!("Error: {}", e),
}

The ? Operator

Propagates errors up the call stack. Equivalent to match with early return Err(...).

use std::io;
use std::fs::File;
use std::io::Read;

fn read_username(path: &str) -> Result<String, io::Error> {
    let mut f = File::open(path)?;     // return Err if open fails
    let mut s = String::new();
    f.read_to_string(&mut s)?;         // return Err if read fails
    Ok(s)
}

// Chaining with ?
fn read_and_parse(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
    let s = std::fs::read_to_string(path)?;
    let n: i32 = s.trim().parse()?;    // ParseIntError auto-converted
    Ok(n * 2)
}

// ? uses From to convert error types:
// err? is equivalent to:
// match err { Ok(v) => v, Err(e) => return Err(From::from(e)) }

? in main (requires main to return Result):

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

? with Option (in functions returning Option):

fn first_double(v: &[i32]) -> Option<i32> {
    let first = v.first()?;    // return None if empty
    Some(first * 2)
}

Unwrapping (Quick and Dirty)

let n: i32 = "42".parse().unwrap();           // panics if Err
let n: i32 = "42".parse().expect("not a number"); // panics with message
let n: i32 = "42".parse().unwrap_or(0);       // default on error
let n: i32 = "42".parse().unwrap_or_else(|e| { eprintln!("{}", e); 0 });
let n: i32 = "abc".parse().unwrap_or_default(); // i32::default() = 0

Use unwrap/expect only in tests, prototypes, or when you know it cannot fail.

Result Methods

let ok: Result<i32, String> = Ok(42);
let err: Result<i32, String> = Err("oops".to_string());

// Checking
ok.is_ok()    // true
ok.is_err()   // false

// Extracting
ok.ok()       // Option<i32>  — Some(42)
ok.err()      // Option<String> — None
ok.as_ref()   // Result<&i32, &String>
ok.as_mut()   // Result<&mut i32, &mut String>

// Transforming success
ok.map(|x| x * 2)                    // Ok(84)
ok.and_then(|x| Ok(x + 1))          // Ok(43) — flatMap
ok.and(Ok("new value"))              // Ok("new value") if self is Ok

// Transforming error
err.map_err(|e| format!("!{}", e))  // Err("!oops")
err.or(Ok(0))                        // Ok(0) — fallback value
err.or_else(|e| Err(e.len()))       // Err(4)

// Consuming
ok.into_ok().unwrap_or(0)           // i32 (nightly: into_ok())
ok.unwrap_or_else(|e| panic!("{}", e))

Defining Custom Error Types

Simple Error Enum

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

#[derive(Debug)]
enum AppError {
    ParseError(ParseIntError),
    DivisionByZero,
    InvalidInput(String),
    IoError(std::io::Error),
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppError::ParseError(e) => write!(f, "parse error: {}", e),
            AppError::DivisionByZero => write!(f, "division by zero"),
            AppError::InvalidInput(s) => write!(f, "invalid input: {}", s),
            AppError::IoError(e) => write!(f, "IO error: {}", e),
        }
    }
}

// Implement std::error::Error (required for ? coercions and Box<dyn Error>)
impl std::error::Error for AppError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            AppError::ParseError(e) => Some(e),
            AppError::IoError(e) => Some(e),
            _ => None,
        }
    }
}

// Implement From for automatic ? conversion
impl From<ParseIntError> for AppError {
    fn from(e: ParseIntError) -> Self { AppError::ParseError(e) }
}

impl From<std::io::Error> for AppError {
    fn from(e: std::io::Error) -> Self { AppError::IoError(e) }
}

fn process(s: &str) -> Result<i32, AppError> {
    let n: i32 = s.parse()?;   // ParseIntError → AppError via From
    if n == 0 { return Err(AppError::DivisionByZero); }
    Ok(100 / n)
}

Box\<dyn Error\> — Erasing Error Types

When you don't care about the specific error type:

use std::error::Error;

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

Downside: you lose the ability to match on specific error variants.

Panicking

For unrecoverable errors or programmer mistakes — not for recoverable errors.

panic!("something went terribly wrong");
panic!("index {} out of bounds (len {})", i, len);

// Other ways to panic:
unwrap() / expect()  // on None or Err
assert!(condition, "message");
assert_eq!(a, b);
assert_ne!(a, b);
unreachable!("should never reach here");
todo!("not implemented yet");
unimplemented!();

// Catch a panic (use sparingly — mainly for FFI or test harnesses)
use std::panic;
let result = panic::catch_unwind(|| {
    panic!("oops");
});
assert!(result.is_err());

Error Propagation Patterns

// Collecting results — fail on first error
let strings = vec!["1", "2", "3", "abc", "5"];
let numbers: Result<Vec<i32>, _> = strings.iter()
    .map(|s| s.parse::<i32>())
    .collect();  // Err(ParseIntError) — stops at "abc"

// Collect all results, keeping both Ok and Err
let results: Vec<Result<i32, _>> = strings.iter()
    .map(|s| s.parse::<i32>())
    .collect();

// Partition into successes and failures
let (successes, failures): (Vec<_>, Vec<_>) = strings.iter()
    .map(|s| s.parse::<i32>())
    .partition(Result::is_ok);

// Skip errors, keep successes
let numbers: Vec<i32> = strings.iter()
    .filter_map(|s| s.parse().ok())
    .collect();  // [1, 2, 3, 5]

// Log errors, keep successes
let numbers: Vec<i32> = strings.iter()
    .filter_map(|s| {
        s.parse().map_err(|e| eprintln!("parse error: {}", e)).ok()
    })
    .collect();

Error Conversion Hierarchy

// From hierarchy: specific → general
impl From<std::num::ParseIntError> for AppError { ... }
impl From<std::io::Error> for AppError { ... }
impl From<AppError> for Box<dyn std::error::Error> { ... }  // auto via std

// ? uses From::from to convert
fn example() -> Result<(), AppError> {
    let _n: i32 = "abc".parse()?;  // ParseIntError → AppError via From
    Ok(())
}

std::io Error Kinds

use std::io::{self, ErrorKind};

fn handle_io_error(e: io::Error) {
    match e.kind() {
        ErrorKind::NotFound => println!("file not found"),
        ErrorKind::PermissionDenied => println!("permission denied"),
        ErrorKind::ConnectionRefused => println!("connection refused"),
        ErrorKind::ConnectionReset => println!("connection reset"),
        ErrorKind::ConnectionAborted => println!("connection aborted"),
        ErrorKind::AddrInUse => println!("address in use"),
        ErrorKind::BrokenPipe => println!("broken pipe"),
        ErrorKind::AlreadyExists => println!("already exists"),
        ErrorKind::WouldBlock => println!("would block"),
        ErrorKind::InvalidInput => println!("invalid input"),
        ErrorKind::InvalidData => println!("invalid data"),
        ErrorKind::TimedOut => println!("timed out"),
        ErrorKind::UnexpectedEof => println!("unexpected EOF"),
        ErrorKind::OutOfMemory => println!("out of memory"),
        _ => println!("other: {}", e),
    }
}

// Create io errors
let e = io::Error::new(ErrorKind::NotFound, "custom message");
let e = io::Error::from(ErrorKind::InvalidInput);