Skip to content

Rust Error Handling — Result, Option & Panic

DodaTech Updated 2026-06-21 7 min read

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

Rust error handling uses the Result and Option types to represent recoverable errors, and panic! for unrecoverable failures, providing type-safe error management without exceptions.

What You'll Learn

In this tutorial, you'll learn how Rust handles errors with Result<T, E> and Option<T>, how the ? operator propagates errors, how to define custom error types, and when to use panic! vs recoverable errors.

Why It Matters

Exception-based error handling in C++ and Java can silently swallow errors or unwind through critical code. Rust's Result type forces you to handle every error at compile time. In systems programming, where a failed file read or network timeout can have cascading consequences, this compile-time guarantee is invaluable.

Real-World Use

File system drivers must handle disk errors gracefully without crashing the kernel. Web servers recover from malformed requests using Result types. Durga Antivirus Pro's scan engine uses Result to handle permission errors, corrupt files, and I/O timeouts without crashing the entire scan pipeline.

flowchart TD
    A[Operation] --> B{Can it fail?}
    B -->|No| C[Return T]
    B -->|Yes, recoverable| D[Return Result]
    B -->|Yes, unrecoverable| E[panic!]
    D --> F{Caller handles?}
    F -->|Yes| G[Match on Ok/Err]
    F -->|No| H[Propagate with ?]
    H --> I[Up the call stack]
â„šī¸ Info

Prerequisites: Structs & Enums and Ownership. Understanding Rust enums is essential.

Result and Option Basics

Result<T, E> represents either success (Ok(T)) or failure (Err(E)). Option<T> represents either a value (Some(T)) or absence (None).

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

fn main() {
    let result = divide(10.0, 2.0);
    match result {
        Ok(value) => println!("Result: {}", value),
        Err(msg) => println!("Error: {}", msg),
    }

    let bad = divide(10.0, 0.0);
    match bad {
        Ok(value) => println!("Result: {}", value),
        Err(msg) => println!("Error: {}", msg),
    }
}

Expected output:

Result: 5
Error: division by zero

The ? Operator

The ? operator propagates errors to the caller, making code concise without losing type safety.

use std::fs::File;
use std::io::{self, Read};

fn read_file_contents(path: &str) -> io::Result<String> {
    let mut file = File::open(path)?; // returns Err immediately if file doesn't exist
    let mut contents = String::new();
    file.read_to_string(&mut contents)?; // returns Err if read fails
    Ok(contents)
}

fn get_file_length(path: &str) -> io::Result<usize> {
    let contents = read_file_contents(path)?;
    Ok(contents.len())
}

fn main() {
    match get_file_length("Cargo.toml") {
        Ok(len) => println!("File length: {} bytes", len),
        Err(e) => println!("Error reading file: {}", e),
    }
}

Expected output:

File length: [number] bytes

Chaining with Combinators

Result and Option provide combinator methods for functional error handling.

fn parse_number(s: &str) -> Option<i32> {
    s.parse().ok()
}

fn main() {
    // Combinator chaining
    let result = parse_number("42")
        .map(|n| n * 2)
        .filter(|n| *n > 50)
        .map(|n| format!("Result: {}", n))
        .unwrap_or_else(|| "Failed to process".to_string());
    println!("{}", result);

    // and_then for fallible chaining
    let sum = parse_number("10")
        .and_then(|a| parse_number("20").map(|b| a + b));
    println!("Sum: {:?}", sum);

    // or_else for recovery
    let val = parse_number("not_a_number")
        .or_else(|| Some(0));
    println!("Defaulted: {:?}", val);
}

Expected output:

Result: 84
Sum: Some(30)
Defaulted: Some(0)

Custom Error Types

For larger projects, define custom error types using the thiserror crate or manual implementations.

use std::fmt;
use std::io;

#[derive(Debug)]
enum ScanError {
    IoError(io::Error),
    ParseError { line: usize, message: String },
    Timeout,
}

impl fmt::Display for ScanError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            ScanError::IoError(e) => write!(f, "I/O error: {}", e),
            ScanError::ParseError { line, message } => {
                write!(f, "Parse error at line {}: {}", line, message)
            }
            ScanError::Timeout => write!(f, "Operation timed out"),
        }
    }
}

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

fn scan_file(path: &str) -> Result<String, ScanError> {
    let contents = std::fs::read_to_string(path)?; // io::Error -> ScanError via From
    if contents.is_empty() {
        return Err(ScanError::ParseError {
            line: 0,
            message: "Empty file".into(),
        });
    }
    Ok(contents)
}

fn main() {
    match scan_file("Cargo.toml") {
        Ok(contents) => println!("Scanned {} bytes", contents.len()),
        Err(e) => println!("Scan failed: {}", e),
    }
}

Expected output:

Scanned [number] bytes

When to panic!

Panic is for unrecoverable errors: array index out of bounds, division by zero, assertion failures. In library code, prefer returning Result. In application code, panic at the top level.

fn main() {
    // These panic
    // let v = vec![1, 2, 3];
    // println!("{}", v[10]); // index out of bounds

    // Using expect for clear panic messages
    let config = std::env::var("CONFIG_PATH")
        .expect("CONFIG_PATH environment variable is required");
    println!("Config path: {}", config);
}

Error Handling and Security

Proper error handling is a security concern. Leaking internal details in error messages can aid attackers. Rust's error types let you control exactly what information is exposed:

  • User-facing errors: Generic messages without internals
  • Internal logging: Full error details for debugging
  • No stack unwinding in release: Prevent information leaks via panic messages

Durga Antivirus Pro logs full error details internally but returns sanitized messages to the user, preventing information disclosure while enabling debugging.

Common Mistakes

1. Using unwrap() in Production Code

unwrap() panics on error. Use ?, match, or combinators instead. Reserve unwrap() for prototyping and tests.

2. Swallowing Errors with ok()

Calling .ok() on a Result discards the error. Prefer .map_err() or .unwrap_or_else() to handle errors properly.

3. Boxing All Errors

Using Box<dyn Error> for every error type loses type information. Define custom enums for known error cases.

4. Panicking in Library Code

Libraries should return Result, not panic. Panicking affects the caller and may cause unexpected program termination.

5. Ignoring the Error Type's Display Implementation

Custom error types should implement both Display and Debug for proper error reporting.

Practice Questions

1. What is the difference between Result and Option? Result<T, E> represents success or failure with an error value. Option<T> represents presence or absence of a value. Use Result when failure provides information, Option when absence is the only failure mode.

2. How does the ? operator work? The ? operator unwraps a Result or Option. If the value is Ok/Some, it unwraps. If Err/None, it returns early from the function, propagating the error.

3. When should you use panic!? Use panic! for unrecoverable errors: out-of-bounds access, assertion failures, invariant violations. Use Result for recoverable errors: file not found, parse failure, network timeout.

4. Why is unwrap() discouraged in production? unwrap() panics on error, causing program termination. Production code should handle errors gracefully, not crash.

5. Challenge: Create a Config struct that parses key=value pairs from a file, using a custom ConfigError enum with variants for Io, Parse, and DuplicateKey errors.

Mini Project: Safe File Scanner

use std::fs::File;
use std::io::{self, BufRead, BufReader};

#[derive(Debug)]
enum FileScanError {
    IoError(io::Error),
    InvalidLine { line_number: usize, content: String },
    SignatureFound(String),
}

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

fn scan_file_for_patterns(path: &str, patterns: &[&str]) -> Result<Vec<String>, FileScanError> {
    let file = File::open(path)?;
    let reader = BufReader::new(file);
    let mut matches = Vec::new();

    for (line_num, line) in reader.lines().enumerate() {
        let line = line?;
        if line.is_empty() {
            continue;
        }
        for pattern in patterns {
            if line.contains(pattern) {
                matches.push(format!("Line {}: {}", line_num + 1, line.trim()));
            }
        }
    }
    Ok(matches)
}

fn main() -> Result<(), FileScanError> {
    let matches = scan_file_for_patterns("Cargo.toml", &["rust", "error"])?;
    if matches.is_empty() {
        println!("No matches found");
    } else {
        println!("Found {} matches:", matches.len());
        for m in matches {
            println!("  {}", m);
        }
    }
    Ok(())
}

FAQ

What is the difference between unwrap and expect?

Both panic on error, but expect takes a custom message that helps identify where the panic occurred. Prefer expect over unwrap for clearer debugging.

Should I use panic! or ? for error handling?

Use ? to propagate errors to callers (recoverable). Use panic! only for truly unrecoverable situations like invariant violations. Libraries should almost never panic.

How do I convert between different error types?

Implement From<E> for your error type. The ? operator automatically calls From::from() to convert. Use map_err() for one-off conversions without implementing From.

Structs & Enums
Traits & Generics
Testing & Documentation

What's Next

Explore Traits & Generics for polymorphism, and Testing & Documentation for writing robust, tested code.

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

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro