1. rust
  2. /structs enums
  3. /option-result

Option & Result

Option<T> and Result<T, E> are fundamental types in Rust that eliminate null pointer exceptions and provide structured error handling. They represent computations that might fail or return no value, forcing you to handle these cases explicitly.

Option<T> - Handling Null Values

Option<T> represents a value that might be present (Some(T)) or absent (None). It's Rust's solution to null safety.

Basic Option Usage

fn find_word(text: &str, word: &str) -> Option<usize> {
    text.find(word)
}

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

fn get_first_char(s: &str) -> Option<char> {
    s.chars().next()
}

fn main() {
    let text = "Hello, world!";
    
    // Pattern matching
    match find_word(text, "world") {
        Some(index) => println!("Found 'world' at index: {}", index),
        None => println!("'world' not found"),
    }
    
    // Using if let
    if let Some(first_char) = get_first_char(text) {
        println!("First character: {}", first_char);
    }
    
    // Handling division
    match divide(10.0, 2.0) {
        Some(result) => println!("10.0 / 2.0 = {}", result),
        None => println!("Cannot divide by zero"),
    }
}

Option Methods

Option<T> provides many useful methods for safe value handling:

Checking Contents

fn option_checking_examples() {
    let some_value = Some(42);
    let no_value: Option<i32> = None;
    
    // Check if value exists
    println!("some_value is_some: {}", some_value.is_some());
    println!("some_value is_none: {}", some_value.is_none());
    println!("no_value is_some: {}", no_value.is_some());
    println!("no_value is_none: {}", no_value.is_none());
    
    // Check if value matches condition
    println!("Contains 42: {}", some_value.contains(&42));
    println!("Contains 10: {}", some_value.contains(&10));
}

Extracting Values

fn option_extraction_examples() {
    let some_value = Some(42);
    let no_value: Option<i32> = None;
    
    // unwrap: Extract value or panic
    // println!("Value: {}", no_value.unwrap()); // This would panic!
    println!("Value: {}", some_value.unwrap());
    
    // unwrap_or: Provide default value
    println!("Value or default: {}", some_value.unwrap_or(0));
    println!("Value or default: {}", no_value.unwrap_or(0));
    
    // unwrap_or_else: Compute default value lazily
    println!("Value or computed: {}", no_value.unwrap_or_else(|| {
        println!("Computing default...");
        100
    }));
    
    // expect: Like unwrap but with custom panic message
    println!("Expected value: {}", some_value.expect("Should have a value"));
}

Transforming Values

fn option_transformation_examples() {
    let some_number = Some(5);
    let no_number: Option<i32> = None;
    
    // map: Transform the contained value
    let doubled = some_number.map(|x| x * 2);
    let doubled_none = no_number.map(|x| x * 2);
    println!("Doubled: {:?}", doubled);       // Some(10)
    println!("Doubled none: {:?}", doubled_none); // None
    
    // map_or: Transform with default
    let result = some_number.map_or(0, |x| x * 3);
    let result_none = no_number.map_or(0, |x| x * 3);
    println!("Tripled or 0: {}", result);      // 15
    println!("Tripled none or 0: {}", result_none); // 0
    
    // map_or_else: Transform with computed default
    let result = no_number.map_or_else(|| {
        println!("Computing default for map");
        -1
    }, |x| x * 4);
    println!("Quadrupled or computed: {}", result); // -1
}

Chaining Operations

fn option_chaining_examples() {
    let some_text = Some("42");
    let no_text: Option<&str> = None;
    let invalid_text = Some("abc");
    
    // and_then: Chain operations that return Option
    let parsed = some_text.and_then(|s| s.parse::<i32>().ok());
    let parsed_none = no_text.and_then(|s| s.parse::<i32>().ok());
    let parsed_invalid = invalid_text.and_then(|s| s.parse::<i32>().ok());
    
    println!("Parsed: {:?}", parsed);         // Some(42)
    println!("Parsed none: {:?}", parsed_none);   // None
    println!("Parsed invalid: {:?}", parsed_invalid); // None
    
    // filter: Keep value only if predicate is true
    let filtered = some_text
        .and_then(|s| s.parse::<i32>().ok())
        .filter(|&x| x > 40);
    println!("Filtered (>40): {:?}", filtered); // Some(42)
    
    let filtered_out = some_text
        .and_then(|s| s.parse::<i32>().ok())
        .filter(|&x| x > 50);
    println!("Filtered (>50): {:?}", filtered_out); // None
}

Result<T, E> - Error Handling

Result<T, E> represents operations that can succeed (Ok(T)) or fail (Err(E)). It's Rust's primary error handling mechanism.

Basic Result Usage

use std::num::ParseIntError;

fn parse_number(s: &str) -> Result<i32, ParseIntError> {
    s.parse()
}

fn divide_numbers(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        Err("Division by zero".to_string())
    } else {
        Ok(a / b)
    }
}

fn read_file_size(path: &str) -> Result<u64, std::io::Error> {
    use std::fs;
    let metadata = fs::metadata(path)?;
    Ok(metadata.len())
}

fn main() {
    // Pattern matching with Result
    match parse_number("42") {
        Ok(num) => println!("Parsed number: {}", num),
        Err(e) => println!("Parse error: {}", e),
    }
    
    // Using if let for success case
    if let Ok(result) = divide_numbers(10, 2) {
        println!("Division result: {}", result);
    }
    
    // Handling both cases
    match divide_numbers(10, 0) {
        Ok(result) => println!("Result: {}", result),
        Err(e) => println!("Error: {}", e),
    }
}

Result Methods

Result<T, E> provides methods similar to Option<T> but for error handling:

Checking Results

fn result_checking_examples() {
    let good_result: Result<i32, &str> = Ok(42);
    let bad_result: Result<i32, &str> = Err("something went wrong");
    
    // Check if result is success or error
    println!("good_result is_ok: {}", good_result.is_ok());
    println!("good_result is_err: {}", good_result.is_err());
    println!("bad_result is_ok: {}", bad_result.is_ok());
    println!("bad_result is_err: {}", bad_result.is_err());
    
    // Check if result contains specific value
    println!("Contains Ok(42): {}", good_result.contains(&42));
    println!("Contains Err: {}", bad_result.contains_err(&"something went wrong"));
}

Extracting Values

fn result_extraction_examples() {
    let good_result: Result<i32, &str> = Ok(42);
    let bad_result: Result<i32, &str> = Err("error");
    
    // unwrap: Extract value or panic on error
    println!("Good result: {}", good_result.unwrap());
    // println!("Bad result: {}", bad_result.unwrap()); // This would panic!
    
    // unwrap_or: Provide default value for error case
    println!("Good or default: {}", good_result.unwrap_or(0));
    println!("Bad or default: {}", bad_result.unwrap_or(0));
    
    // unwrap_err: Extract error or panic on success
    // println!("Good error: {}", good_result.unwrap_err()); // This would panic!
    println!("Bad error: {}", bad_result.unwrap_err());
    
    // expect: Like unwrap but with custom panic message
    println!("Expected good: {}", good_result.expect("Should be OK"));
}

Transforming Results

fn result_transformation_examples() {
    let good_result: Result<i32, &str> = Ok(5);
    let bad_result: Result<i32, &str> = Err("error");
    
    // map: Transform success value
    let doubled = good_result.map(|x| x * 2);
    let doubled_err = bad_result.map(|x| x * 2);
    println!("Doubled good: {:?}", doubled);     // Ok(10)
    println!("Doubled bad: {:?}", doubled_err);  // Err("error")
    
    // map_err: Transform error value
    let mapped_error = bad_result.map_err(|e| format!("Error: {}", e));
    println!("Mapped error: {:?}", mapped_error); // Err("Error: error")
    
    // map_or: Transform success with default for error
    let result = good_result.map_or(0, |x| x * 3);
    let result_err = bad_result.map_or(0, |x| x * 3);
    println!("Tripled or 0: {}", result);        // 15
    println!("Error or 0: {}", result_err);      // 0
}

The ? Operator

The ? operator provides concise error propagation:

Basic Error Propagation

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

fn read_username_from_file() -> Result<String, io::Error> {
    let mut file = File::open("username.txt")?;
    let mut username = String::new();
    file.read_to_string(&mut username)?;
    Ok(username)
}

// Equivalent to the above, but more verbose
fn read_username_from_file_verbose() -> Result<String, io::Error> {
    let file = File::open("username.txt");
    let mut file = match file {
        Ok(file) => file,
        Err(e) => return Err(e),
    };
    
    let mut username = String::new();
    match file.read_to_string(&mut username) {
        Ok(_) => Ok(username),
        Err(e) => Err(e),
    }
}

fn calculate_sum(a: &str, b: &str) -> Result<i32, std::num::ParseIntError> {
    let num_a = a.parse::<i32>()?;
    let num_b = b.parse::<i32>()?;
    Ok(num_a + num_b)
}

fn main() {
    match calculate_sum("10", "20") {
        Ok(sum) => println!("Sum: {}", sum),
        Err(e) => println!("Parse error: {}", e),
    }
}

Converting Between Error Types

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

#[derive(Debug)]
enum MyError {
    Parse(ParseIntError),
    DivisionByZero,
    Negative,
}

impl fmt::Display for MyError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            MyError::Parse(e) => write!(f, "Parse error: {}", e),
            MyError::DivisionByZero => write!(f, "Division by zero"),
            MyError::Negative => write!(f, "Negative result not allowed"),
        }
    }
}

impl From<ParseIntError> for MyError {
    fn from(error: ParseIntError) -> Self {
        MyError::Parse(error)
    }
}

fn safe_divide(a: &str, b: &str) -> Result<f64, MyError> {
    let num_a = a.parse::<f64>()?; // Automatic conversion via From trait
    let num_b = b.parse::<f64>()?;
    
    if num_b == 0.0 {
        return Err(MyError::DivisionByZero);
    }
    
    let result = num_a / num_b;
    if result < 0.0 {
        return Err(MyError::Negative);
    }
    
    Ok(result)
}

fn main() {
    match safe_divide("10", "2") {
        Ok(result) => println!("Result: {}", result),
        Err(e) => println!("Error: {}", e),
    }
}

Converting Between Option and Result

You can convert between Option and Result:

fn conversion_examples() {
    let some_value = Some(42);
    let no_value: Option<i32> = None;
    
    // Option to Result
    let result_from_some = some_value.ok_or("No value");
    let result_from_none = no_value.ok_or("No value");
    println!("Some to Result: {:?}", result_from_some); // Ok(42)
    println!("None to Result: {:?}", result_from_none); // Err("No value")
    
    // Result to Option
    let good_result: Result<i32, &str> = Ok(100);
    let bad_result: Result<i32, &str> = Err("error");
    
    let option_from_ok = good_result.ok();
    let option_from_err = bad_result.ok();
    println!("Ok to Option: {:?}", option_from_ok);     // Some(100)
    println!("Err to Option: {:?}", option_from_err);   // None
    
    // Extract error as Option
    let error_option = bad_result.err();
    println!("Error as Option: {:?}", error_option);    // Some("error")
}

Practical Patterns

Safe Indexing

fn safe_indexing_examples() {
    let numbers = vec![1, 2, 3, 4, 5];
    
    // Safe get with Option
    match numbers.get(10) {
        Some(value) => println!("Value at index 10: {}", value),
        None => println!("Index 10 is out of bounds"),
    }
    
    // Chain operations safely
    let result = numbers
        .get(2)
        .map(|x| x * 2)
        .filter(|&x| x > 5)
        .unwrap_or(0);
    println!("Processed value: {}", result);
}

Configuration Parsing

use std::collections::HashMap;

struct Config {
    host: String,
    port: u16,
    debug: bool,
}

impl Config {
    fn from_map(map: &HashMap<String, String>) -> Result<Config, String> {
        let host = map.get("host")
            .ok_or("Missing host")?
            .clone();
        
        let port = map.get("port")
            .ok_or("Missing port")?
            .parse::<u16>()
            .map_err(|_| "Invalid port number")?;
        
        let debug = map.get("debug")
            .map(|s| s == "true")
            .unwrap_or(false);
        
        Ok(Config { host, port, debug })
    }
}

fn main() {
    let mut config_map = HashMap::new();
    config_map.insert("host".to_string(), "localhost".to_string());
    config_map.insert("port".to_string(), "8080".to_string());
    config_map.insert("debug".to_string(), "true".to_string());
    
    match Config::from_map(&config_map) {
        Ok(config) => println!("Config: {}:{}, debug: {}", 
                              config.host, config.port, config.debug),
        Err(e) => println!("Config error: {}", e),
    }
}

Chaining Multiple Fallible Operations

fn process_user_input(input: &str) -> Result<i32, String> {
    input
        .trim()                                    // Remove whitespace
        .parse::<i32>()                           // Parse to integer
        .map_err(|_| "Invalid number format".to_string())? // Convert error
        .checked_mul(2)                           // Safe multiplication
        .ok_or("Multiplication overflow".to_string())? // Handle overflow
        .checked_add(10)                          // Safe addition
        .ok_or("Addition overflow".to_string())   // Handle overflow
}

fn main() {
    let inputs = vec!["42", " 21 ", "abc", "1000000000"];
    
    for input in inputs {
        match process_user_input(input) {
            Ok(result) => println!("'{}' -> {}", input, result),
            Err(e) => println!("'{}' -> Error: {}", input, e),
        }
    }
}

Collecting Results

fn parse_numbers(strings: Vec<&str>) -> Result<Vec<i32>, std::num::ParseIntError> {
    strings.into_iter()
        .map(|s| s.parse::<i32>())
        .collect()  // Collects Result<Vec<i32>, ParseIntError>
}

fn main() {
    let good_strings = vec!["1", "2", "3", "4"];
    let bad_strings = vec!["1", "2", "abc", "4"];
    
    match parse_numbers(good_strings) {
        Ok(numbers) => println!("Parsed numbers: {:?}", numbers),
        Err(e) => println!("Parse error: {}", e),
    }
    
    match parse_numbers(bad_strings) {
        Ok(numbers) => println!("Parsed numbers: {:?}", numbers),
        Err(e) => println!("Parse error: {}", e),
    }
}

Best Practices

1. Prefer Option and Result Over Panics

// Good: Return Option for operations that might fail
fn find_user_by_id(id: u32) -> Option<User> {
    // Search logic...
    None
}

// Good: Return Result for operations with detailed errors
fn validate_email(email: &str) -> Result<(), String> {
    if email.contains('@') {
        Ok(())
    } else {
        Err("Email must contain @".to_string())
    }
}

// Avoid: Panicking on failure
fn bad_find_user(id: u32) -> User {
    // panic!("User not found"); // Don't do this
    User::default()
}

2. Use ? for Error Propagation

// Good: Use ? for clean error propagation
fn process_file(path: &str) -> Result<String, std::io::Error> {
    let content = std::fs::read_to_string(path)?;
    Ok(content.to_uppercase())
}

// Avoid: Manual error handling when ? works
fn process_file_verbose(path: &str) -> Result<String, std::io::Error> {
    match std::fs::read_to_string(path) {
        Ok(content) => Ok(content.to_uppercase()),
        Err(e) => Err(e),
    }
}

3. Use Appropriate Methods

// Good: Use specific methods for different scenarios
fn handle_optional_value(opt: Option<i32>) {
    // Use if let for simple cases
    if let Some(value) = opt {
        println!("Got: {}", value);
    }
    
    // Use unwrap_or for defaults
    let with_default = opt.unwrap_or(0);
    
    // Use map for transformations
    let doubled = opt.map(|x| x * 2);
}

4. Provide Good Error Messages

// Good: Descriptive error types
#[derive(Debug)]
enum ValidationError {
    TooShort { min_length: usize, actual: usize },
    InvalidCharacter { character: char, position: usize },
    Empty,
}

// Good: Helpful error messages
fn validate_password(password: &str) -> Result<(), ValidationError> {
    if password.is_empty() {
        return Err(ValidationError::Empty);
    }
    
    if password.len() < 8 {
        return Err(ValidationError::TooShort {
            min_length: 8,
            actual: password.len(),
        });
    }
    
    Ok(())
}

Option<T> and Result<T, E> are fundamental to safe Rust programming. They force explicit handling of edge cases and errors, eliminating entire classes of runtime bugs. Master these types to write robust, reliable Rust code that gracefully handles failure conditions.