Skip to content

Rust Error Handling Explained — Result, Option, Panic & Custom Error Types

DodaTech Updated 2026-06-23 8 min read

In this tutorial, you'll learn about Rust Error Handling Explained. We cover key concepts, practical examples, and best practices.

Master Rust error handling: Result and Option combinators, the propagation operator, custom error types with thiserror and anyhow, panic hooks, and production error strategies.

What You'll Learn

In this tutorial, you'll master Rust error handling beyond the basics — chaining combinators like map, and_then, or_else, building custom error types with thiserror, using anyhow for application code, setting panic hooks, and designing error strategies for libraries vs binaries.

Why It Matters

Error handling is not just about catching failures — it shapes your API's usability. Libraries with well-designed error types give callers precise control. Applications with good error context reduce debugging time. Rust's type system makes error handling explicit, but choosing the right pattern for your use case separates production-grade code from prototypes.

Real-World Use

The reqwest HTTP library exposes a rich error type hierarchy with variants for timeouts, redirects, and decode failures. The sqlx database library uses anyhow for query errors with context. Durga Antivirus Pro's scan pipeline uses a custom error enum with typed variants for file I/O, permission denied, signature corruption, and scan timeout.

flowchart TD
    A[Operation] --> B{Result or Option?}
    B -->|Some value| C[Ok / Some]
    B -->|No value at all| D[None]
    B -->|Failed with error| E[Err]
    C --> F[Continue processing]
    D --> G[Default / next option]
    E --> H{Propagate or handle?}
    H -->|Propagate| I[? operator]
    H -->|Handle| J[match / combinator]
â„šī¸ Info

Prerequisites: Error Handling basics and Structs & Enums. Understanding traits and generics helps.

Combinators: map, and_then, or_else

Combinators let you chain operations on Result and Option without manual matching:

use std::num::ParseIntError;

fn parse_and_process(input: &str) -> Result<i32, ParseIntError> {
    input
        .parse::<i32>()
        .map(|n| n * 2)
        .map_err(|_| ParseIntError { .. })
}

fn safe_divide(a: f64, b: f64) -> Option<f64> {
    if b == 0.0 {
        None
    } else {
        Some(a / b)
    }
}

fn main() {
    let result = parse_and_process("21");
    println!("Parsed and doubled: {:?}", result);
    
    let division = safe_divide(10.0, 3.0)
        .map(|v| format!("{:.2}", v))
        .or_else(|| Some("undefined".to_string()));
    println!("Division: {}", division.unwrap());
    
    // Chaining with and_then
    let chain = safe_divide(10.0, 2.0)
        .and_then(|v| safe_divide(v, 2.0))
        .and_then(|v| safe_divide(v, 1.25));
    println!("Chained division: {:?}", chain);
}

Expected output:

Parsed and doubled: Ok(42)
Division: 3.33
Chained division: Some(4.0)

Custom Error Types with thiserror

For libraries, define custom error enums with thiserror for automatic Display and Error trait implementations:

use thiserror::Error;
use std::io;

#[derive(Error, Debug)]
pub enum ScanError {
    #[error("I/O error reading file: {0}")]
    Io(#[from] io::Error),
    
    #[error("Invalid signature format at offset {offset}")]
    InvalidSignature { offset: usize },
    
    #[error("Scan timeout after {0}ms")]
    Timeout(u64),
    
    #[error("Permission denied: {0}")]
    Permission(String),
}

fn scan_file(path: &str) -> Result<Vec<String>, ScanError> {
    let data = std::fs::read(path)?; // Auto-converts io::Error via #[from]
    if data.is_empty() {
        return Err(ScanError::InvalidSignature { offset: 0 });
    }
    Ok(vec!["clean".to_string()])
}

fn main() {
    match scan_file("/etc/hosts") {
        Ok(results) => println!("Results: {:?}", results),
        Err(e) => println!("Scan error: {}", e),
    }
    match scan_file("/nonexistent") {
        Ok(results) => println!("Results: {:?}", results),
        Err(e) => println!("Scan error: {}", e),
    }
}

Expected output:

Results: ["clean"]
Scan error: I/O error reading file: No such file or directory (os error 2)
â„šī¸ Info

thiserror vs anyhow: Use thiserror in libraries for typed errors that callers can match on. Use anyhow in binaries and application code for quick error handling with context.

Panic Hooks and Unrecoverable Errors

Custom panic hooks let you log or recover from panics gracefully:

use std::panic;

fn main() {
    // Install a custom panic hook
    panic::set_hook(Box::new(|panic_info| {
        let location = panic_info.location()
            .map(|l| format!("{}:{}", l.file(), l.line()))
            .unwrap_or_else(|| "unknown".into());
        
        let message = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
            s.to_string()
        } else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
            s.clone()
        } else {
            "Unknown panic".to_string()
        };
        
        eprintln!("[CUSTOM HOOK] Panic at {}: {}", location, message);
    }));
    
    // Example: safe array access with explicit panic
    fn get_element(vec: &[i32], idx: usize) -> i32 {
        if idx >= vec.len() {
            panic!("Index {} out of bounds for len {}", idx, vec.len());
        }
        vec[idx]
    }
    
    println!("First: {}", get_element(&[10, 20, 30], 0));
    
    // Use std::panic::catch_unwind to recover
    let result = panic::catch_unwind(|| {
        get_element(&[10, 20, 30], 10)
    });
    match result {
        Ok(val) => println!("Got: {}", val),
        Err(_) => println!("Recovered from panic gracefully"),
    }
    
    println!("Program continues after caught panic");
}

Expected output:

First: 10
[CUSTOM HOOK] Panic at src/main.rs:26: Index 10 out of bounds for len 3
Recovered from panic gracefully
Program continues after caught panic

Error Handling Patterns in Production

Patterns for real codebases: typed errors for libraries, anyhow for apps, backtrace support:

// Pattern 1: Box<dyn Error> for heterogeneous errors
fn read_config(path: &str) -> Result<String, Box<dyn std::error::Error>> {
    let content = std::fs::read_to_string(path)?;
    if content.is_empty() {
        return Err("config file is empty".into());
    }
    Ok(content)
}

// Pattern 2: Result<T, E> with crate-internal error type
#[derive(Debug)]
struct AppError(String);

impl std::fmt::Display for AppError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "AppError: {}", self.0)
    }
}

impl std::error::Error for AppError {}

fn validate_input(input: &str) -> Result<(), AppError> {
    if input.trim().is_empty() {
        Err(AppError("input cannot be blank".into()))
    } else if input.len() > 100 {
        Err(AppError("input exceeds 100 characters".into()))
    } else {
        Ok(())
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = read_config("Cargo.toml")?;
    println!("Config length: {}", config.len());
    
    validate_input("hello")?;
    println!("Input is valid");
    
    let result = validate_input("");
    match result {
        Err(e) => println!("Validation failed: {}", e),
        Ok(_) => println!("OK"),
    }
    
    Ok(())
}

Expected output:

Config length: ... (varies)
Input is valid
Validation failed: AppError: input cannot be blank

Common Errors

1. Ignoring Result with unused Ok

let _ = File::create("test.txt"); // Warning: unused Result
File::create("test.txt")?; // Correct: propagate error

2. Overly broad error type

fn parse() -> Result<i32, Box<dyn Error>> { /* too broad for library */ }

3. Forgetting to implement Display for error enums

#[derive(Debug)]
enum MyError { Io(io::Error) }
// ERROR: does not implement Display, cannot use ?

4. Swallowing errors with ok()

let _ = fs::read_to_string("file").ok(); // Silently ignores error

5. panic in library code

fn divide(a: i32, b: i32) -> i32 {
    if b == 0 { panic!("division by zero"); } // Bad library pattern
    a / b
}
// Better: return Result<i32, DivisionError>

Practice Questions

1. What is the difference between thiserror and anyhow? thiserror generates custom error types with Display and Error impls — use in libraries. anyhow provides anyhow::Error with context methods like .context() — use in binaries.

2. When should you use catch_unwind? Use catch_unwind at thread boundaries (e.g., web server request handlers) to prevent a single panic from crashing the entire process. Do NOT use it for control flow — that's what Result is for.

3. How do you convert between different error types with ? The ? operator uses Into<E>::into() or From<E>::from() to convert errors. Derive #[from] with thiserror or implement From manually.

4. What is the difference between Ok(()) and Ok()? Ok(()) returns a Result with an empty unit value (success with no data). Ok() without the unit would be a curried function, not a Result. Always use Ok(()).

5. Challenge: Build a configuration file parser that reads key=value pairs and returns a HashMap<String, String>. Define a ConfigError enum with variants for Io, Parse, and DuplicateKey errors. Use thiserror for the error type.

Mini Project: Result-Optimized File Scanner

use std::fs;
use std::io;
use std::path::Path;

#[derive(Debug)]
enum Scanned {
    Clean(String),
    Infected(String, Vec<String>),
}

fn scan_directory(path: &Path, patterns: &[&str]) -> Result<Vec<Scanned>, io::Error> {
    let mut results = Vec::new();
    for entry in fs::read_dir(path)? {
        let entry = entry?;
        let path = entry.path();
        if path.is_file() {
            match fs::read_to_string(&path) {
                Ok(content) => {
                    let hits: Vec<String> = patterns
                        .iter()
                        .filter(|p| content.contains(*p))
                        .map(|p| p.to_string())
                        .collect();
                    if hits.is_empty() {
                        results.push(Scanned::Clean(path.to_string_lossy().into()));
                    } else {
                        results.push(Scanned::Infected(path.to_string_lossy().into(), hits));
                    }
                }
                Err(e) if e.kind() == io::ErrorKind::PermissionDenied => {
                    // Log and skip, don't fail the entire scan
                    eprintln!("Skipping {}: permission denied", path.display());
                }
                Err(e) => return Err(e),
            }
        }
    }
    Ok(results)
}

fn main() -> Result<(), io::Error> {
    let results = scan_directory(
        Path::new("/home/admin1/projects/website/dodatech-tutorials"),
        &["rust", "unsafe", "std"],
    )?;
    for r in &results {
        match r {
            Scanned::Clean(name) => println!("CLEAN: {}", name),
            Scanned::Infected(name, hits) => println!("ALERT: {} hit on {:?}", name, hits),
        }
    }
    Ok(())
}

Expected output: (varies by directory contents)

CLEAN: ... (files without matches)
ALERT: ... (files containing "rust", "unsafe", "std")

FAQ

Should I use panic or Result for invalid arguments?

Use Result for recoverable errors where the caller might want to handle the failure. Use panic! or expect only for programming errors that should never happen (e.g., index out of bounds when you control the index). A library should rarely panic — panic is for binaries.

What is the ? operator and how does it work?

The ? operator is syntactic sugar for a match on Result or Option. On Ok(value) or Some(value), it unwraps the value. On Err(e) or None, it returns early from the function, converting the error via From or returning None.

How do I add backtrace information to errors?

Enable RUST_BACKTRACE=1 environment variable for automatic backtrace on panics. For anyhow, use .context() and RUST_LIB_BACKTRACE=1. For custom errors, use std::backtrace::Backtrace or the backtrace crate.

Error Handling Basics
Traits & Generics
Testing in Rust
Concurrency & Async

What's Next

Validate your error handling patterns with Testing in Rust, or see how errors interact with Concurrency & Async in multithreaded contexts.

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro