Rust LogoRecoverable Errors with Result

In Rust, errors are broadly categorized into two types: recoverable and unrecoverable. Recoverable errors are those that you can anticipate and potentially handle, allowing your program to continue running. Unrecoverable errors, on the other hand, indicate a serious problem from which it's unlikely the program can recover, often leading to a 'panic' and program termination.

Rust's primary mechanism for handling recoverable errors is the `Result` enum. `Result` is an enumeration that has two variants:

* `Ok(T)`: Represents success and contains the value `T` that was produced.
* `Err(E)`: Represents failure and contains an error value `E` that explains why the operation failed.

This design encourages explicit error handling, forcing the programmer to consider what might go wrong and how to deal with it, rather than letting errors silently propagate or crash the program unexpectedly. Functions that might fail are designed to return `Result<T, E>` where `T` is the type of the successful value and `E` is the type of the error.

Handling `Result`:

1. `match` Statement: The most explicit way to handle `Result` is using a `match` statement, which allows you to execute different code branches depending on whether the `Result` is `Ok` or `Err`.

```rust
match some_function_that_returns_result() {
Ok(value) => println!("Success: {}", value),
Err(error) => eprintln!("Error: {:?}", error),
}
```

2. `unwrap()` and `expect()`: These methods are used to extract the `Ok` value directly. If the `Result` is `Err`, they will cause the program to `panic!`. `expect()` is similar to `unwrap()` but allows you to provide a custom panic message, which is helpful for debugging.

```rust
let value = some_function_that_returns_result().unwrap(); // Panics if Err
let value = some_function_that_returns_result().expect("Failed to get value!"); // Panics with message if Err
```

These should generally be avoided in production code unless you are absolutely certain the operation will succeed or if panicking is the desired behavior for an unrecoverable error at that specific point.

3. The `?` Operator: The `?` operator is a concise way to propagate errors. It can only be used in functions that return `Result` (or `Option`). When used on a `Result`:

* If the `Result` is `Ok(T)`, the `Ok` value `T` is unwrapped and becomes the value of the expression.
* If the `Result` is `Err(E)`, the `Err` value `E` is immediately returned from the current function. The `E` type must be convertible to the current function's `Err` type (often via the `From` trait).

```rust
fn read_file_contents(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)
}
```

Custom Error Types: For more complex applications, it's common to define custom error enums that can encapsulate different types of errors that might occur within your domain. This allows for more granular error reporting and handling. Often, these custom error types implement the `std::error::Error` trait and use `From` implementations to convert standard library errors (like `std::io::Error` or `std::num::ParseIntError`) into variants of the custom error type, making error propagation with `?` seamless.

Example Code

```rust
use std::fs::File;
use std::io::{self, Read};
use std::num::ParseIntError;

// 1. Define a custom error enum to encapsulate different error types
#[derive(Debug)]
enum AppError {
    Io(io::Error),
    Parse(ParseIntError),
    EmptyFile,
}

// 2. Implement From traits to convert standard errors into AppError easily
//    This allows the '?' operator to automatically convert standard errors
//    into our custom AppError when propagating.
impl From<io::Error> for AppError {
    fn from(err: io::Error) -> AppError {
        AppError::Io(err)
    }
}

impl From<ParseIntError> for AppError {
    fn from(err: ParseIntError) -> AppError {
        AppError::Parse(err)
    }
}

// Function to read a number from a file, parse it, and return it.
// It returns a Result with our custom error type `AppError`.
fn read_and_double_number_from_file(file_path: &str) -> Result<i32, AppError> {
    // Open the file. The '?' operator propagates any io::Error encountered.
    let mut file = File::open(file_path)?;

    let mut contents = String::new();
    // Read file contents into a string. '?' propagates io::Error.
    file.read_to_string(&mut contents)?;

    // Handle case where file is empty
    if contents.trim().is_empty() {
        return Err(AppError::EmptyFile);
    }

    // Trim whitespace and parse the string into an i32.
    // '?' propagates any ParseIntError.
    let number = contents.trim().parse::<i32>()?;

    Ok(number * 2)
}

fn main() {
    let file_path_success = "number_ok.txt";
    let file_path_fail_parse = "number_bad.txt";
    let file_path_fail_io = "nonexistent_file.txt";
    let file_path_empty = "number_empty.txt";

    // Create some dummy files for testing
    std::fs::write(file_path_success, "123\n").expect("Failed to write file");
    std::fs::write(file_path_fail_parse, "hello\n").expect("Failed to write file");
    std::fs::write(file_path_empty, "  \n").expect("Failed to write file"); // Contains only whitespace

    println!("--- Attempting to read and double a valid number ---");
    match read_and_double_number_from_file(file_path_success) {
        Ok(num) => println!("Successfully read, parsed, and doubled: {}", num),
        Err(e) => eprintln!("Error for {}: {:?}", file_path_success, e),
    }

    println!("\n--- Attempting to read and double an invalid number (parse error) ---");
    match read_and_double_number_from_file(file_path_fail_parse) {
        Ok(num) => println!("Successfully read, parsed, and doubled: {}", num),
        Err(e) => eprintln!("Error for {}: {:?}", file_path_fail_parse, e),
    }

    println!("\n--- Attempting to read from an empty file ---");
    match read_and_double_number_from_file(file_path_empty) {
        Ok(num) => println!("Successfully read, parsed, and doubled: {}", num),
        Err(e) => eprintln!("Error for {}: {:?}", file_path_empty, e),
    }

    println!("\n--- Attempting to read a nonexistent file (IO error) ---");
    match read_and_double_number_from_file(file_path_fail_io) {
        Ok(num) => println!("Successfully read, parsed, and doubled: {}", num),
        Err(e) => eprintln!("Error for {}: {:?}", file_path_fail_io, e),
    }

    // Clean up dummy files
    std::fs::remove_file(file_path_success).expect("Failed to remove file");
    std::fs::remove_file(file_path_fail_parse).expect("Failed to remove file");
    std::fs::remove_file(file_path_empty).expect("Failed to remove file");
}
```