Error Handling in Rust

Error handling in Rust is a critical part of writing robust and reliable software. Rust provides powerful tools for handling errors, primarily through the Result and Option types. Below are some common patterns for error handling in Rust.

The Result Type

The Result type is used for functions that can return an error. It is an enum with two variants: Ok(T) for successful results and Err(E) for errors.

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() {
    match divide(4.0, 2.0) {
        Ok(result) => println!("Result: {}", result),
        Err(e) => println!("Error: {}", e),
    }

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

The Option Type

The Option type is used for values that may or may not be present. It is an enum with two variants: Some(T) for a value and None for no value.

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

fn main() {
    match find_word("hello world", "world") {
        Some(index) => println!("Found at index: {}", index),
        None => println!("Not found"),
    }
}

The ? Operator

The ? operator is a shorthand for propagating errors. It can be used in functions that return a Result or Option.

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

fn read_file(path: &str) -> Result<String, std::io::Error> {
    let mut file = std::fs::File::open(path)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

fn main() {
    match read_file("example.txt") {
        Ok(contents) => println!("File contents: {}", contents),
        Err(e) => println!("Error reading file: {}", e),
    }
}

Custom Error Types

You can define your own error types to provide more context about errors. Below is an example of how to create and use custom error types in Rust.

use std::fmt;

#[derive(Debug)]
enum MyError {
    IoError(std::io::Error),
    ParseError(std::num::ParseIntError),
}

impl fmt::Display for MyError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            MyError::IoError(ref err) => write!(f, "IO error: {}", err),
            MyError::ParseError(ref err) => write!(f, "Parse error: {}", err),
        }
    }
}

impl From<std::io::Error> for MyError {
    fn from(err: std::io::Error) -> MyError {
        MyError::IoError(err)
    }
}

impl From<std::num::ParseIntError> for MyError {
    fn from(err: std::num::ParseIntError) -> MyError {
        MyError::ParseError(err)
    }
}

fn read_and_parse_file(path: &str) -> Result<i32, MyError> {
    let contents = std::fs::read_to_string(path)?;
    let number: i32 = contents.trim().parse()?;
    Ok(number)
}

fn main() {
    match read_and_parse_file("number.txt") {
        Ok(number) => println!("Parsed number: {}", number),
        Err(e) => println!("Error: {}", e),
    }
}

In this example, we define a custom error type MyError that can represent both I/O errors and parsing errors. We then implement the fmt::Display trait for MyError to provide a user-friendly error message. Additionally, we implement the From trait to convert from std::io::Error and std::num::ParseIntError to MyError. This allows us to use the ? operator to propagate these errors in the read_and_parse_file function.

These patterns cover the basics of error handling in Rust. By using Result, Option, the ? operator, and custom error types, you can write code that gracefully handles errors and provides useful feedback.