Error Handling

Master different error handling techniques in Rust, including `Result` type and `panic!` macro.


Handling Result with match in Rust

The Result enum in Rust is a powerful tool for handling operations that might fail. It has two variants: Ok(T), which represents success and contains a value of type T, and Err(E), which represents failure and contains an error value of type E. The match expression provides a robust and explicit way to handle both these possibilities.

Using match for Explicit Result Handling

The match expression allows you to examine the Result value and execute different code blocks based on whether it's an Ok or an Err. This forces you to explicitly acknowledge and handle potential errors, making your code more reliable.

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

fn main() {
    let result = divide(10, 2);

    match result {
        Ok(quotient) => {
            println!("The quotient is: {}", quotient);
        }
        Err(error) => {
            println!("Error: {}", error);
        }
    }

    let another_result = divide(5, 0);

    match another_result {
        Ok(quotient) => {
            println!("The quotient is: {}", quotient);
        }
        Err(error) => {
            println!("Error: {}", error);
        }
    }
} 

Explanation:

  • The divide function attempts to divide two integers. If the divisor is zero, it returns an Err with a descriptive error message. Otherwise, it returns an Ok containing the result of the division.
  • In main, we call divide and store the returned Result in the result variable.
  • The match expression then checks the value of result:
    • If it's Ok(quotient), the code block associated with the Ok pattern is executed. The value inside the Ok variant (the quotient) is bound to the variable quotient, which can then be used within that block.
    • If it's Err(error), the code block associated with the Err pattern is executed. The error message is bound to the variable error, which can then be used to display the error.
  • The example demonstrates how the match handles both a successful division (10 / 2) and a division by zero.

Different Patterns and Error Handling Strategies

The match expression can be used with more complex patterns and error handling strategies.

 #[derive(Debug)]
enum MathError {
    DivisionByZero,
    NegativeRoot,
}

fn safe_sqrt(number: f64) -> Result<f64, MathError> {
    if number < 0.0 {
        Err(MathError::NegativeRoot)
    } else {
        Ok(number.sqrt())
    }
}

fn safe_divide(a: i32, b: i32) -> Result<i32, MathError> {
    if b == 0 {
        Err(MathError::DivisionByZero)
    } else {
        Ok(a / b)
    }
}


fn main() {
    let sqrt_result = safe_sqrt(-1.0);
    match sqrt_result {
        Ok(val) => println!("Square root: {}", val),
        Err(MathError::NegativeRoot) => println!("Error: Cannot take the square root of a negative number"),
        Err(err) => println!("An unexpected error occurred: {:?}", err), // Fallback for other errors
    }


    let division_result = safe_divide(10,0);
    match division_result {
        Ok(val) => println!("Result of division is : {}", val),
        Err(MathError::DivisionByZero) => println!("Attempted division by zero."),
        Err(err) => println!("Other Errors: {:?}", err),
    }
} 

Explanation:

  • Custom Error Types: The code defines a custom enum MathError to represent specific error conditions (division by zero, negative root). This makes the error handling more precise.
  • Specific Error Matching: The match expression can match on specific error variants within the Err arm. For example, Err(MathError::NegativeRoot) handles only the case where a negative number is passed to safe_sqrt.
  • Fallback Error Handling: The Err(err) case acts as a catch-all, handling any other error variants that might occur. This is important to ensure that all possible error conditions are addressed, even if you don't have specific handling for each one. The {:?} formatter used with `println!` will print the debug representation of the error variant.

Benefits of Using match

  • Exhaustive Handling: The compiler forces you to handle both Ok and Err cases.
  • Pattern Matching: Allows you to extract values from Ok and match specific error variants.
  • Readability: Makes your code clearer and easier to understand.
  • Safety: Reduces the risk of unhandled errors leading to unexpected behavior.