Rust by Example: Error Handling

Rust provides a concise and expressive error-handling mechanism using the Result<T, E> enum. It has two variants: Ok(T) for success and Err(E) for errors. T is the type of the success value, and E is the type of the error.

Run code Copy code
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {

Returning Errors

To return errors in Rust, we use the Result<T, E> type. Here is a simple function that performs division and returns a Result<i32, String>.

    fn divide(x: i32, y: i32) -> Result<i32, String> {
        if y == 0 {
            return Err("Division by zero".to_string());
        }
        Ok(x / y)
    }

Performs checked multiplication; returns an error when an integer overflow occurs.

    fn multiply(a: i32, b: i32) -> Result<i32, String> {
        a.checked_mul(b)
            .ok_or("Integer overflow on multiplication".to_string())
    }

To signify the absence of a value upon success, we can use Result<()>. Ok(()) indicates success, while Err(e) indicates an error. Using (), an empty tuple, signifies the absence of a value.

    fn do_something() -> Result<(), String> {
        Ok(())
    }

Uses Box<dyn Error> to return different error types uniformly.

    fn divide_and_multiply(
        i: i32,
        j: i32,
        k: i32,
    ) -> Result<i32, Box<dyn Error>> {
        let division = divide(i, j)?;
        let product = multiply(division, k)?;
        Ok(product)
    }

Important to note is that we lose the specific error type information when using Box<dyn Error>. For real-world applications, creating custom error types is often a better approach as it provides more specific error information.

Result type alias is a kind of shorthand that can be used to simplify function signatures. Instead of writing Result<T, Box<dyn Error>> every time, we can define a type alias like this.

    type MyResult<T> = Result<T, Box<dyn Error>>;

Example function using the MyResult type alias.

    fn divide_and_multiply_alias(i: i32, j: i32, k: i32) -> MyResult<i32> {
        let calc_result = divide_and_multiply(i, j, k)?;
        Ok(calc_result)
    }

Handling Errors

Pattern Matching

One way to handle errors is to use a match expression. Here we call the divide function and pattern match on the result to handle the success and error cases.

    match divide(10, 2) {
        Ok(division_str) => println!("Success: {}", division_str),
        Err(error) => println!("Error: {}", error),
    }

Other pattern matching techniques for handling Result types are if let, and let else. This can be useful when you want to handle errors early and avoid nested code, and less verbose than match.

Use if let when you want to handle both success and error cases.

    if let Ok(division_str) = divide(10, 2) {
        println!("if let success: {}", division_str);
    } else {
        eprintln!("if let error occurred");
    }

Use let else when you want to extract a value and exit early on error. Making the success value available for the rest of the function scope.

    let Ok(division_str) = divide(20, 4) else {
        return Err("let else encountered an error".into());
    };
    println!("let else success: {}", division_str);

Result Methods

Additionally the Result<T, E> type provides several useful methods for working with results. Here are some commonly used methods:

result.ok() extracts the value from an Ok variant, discarding the error and returning Some(value).

    if let Some(value) = divide(15, 3).ok() {
        println!("Converted to Option: {}", value);
    }

result.err() returns the error value, if any as an Option<E>, discarding the success value.

    if let Some(err) = divide(10, 0).err() {
        eprintln!("Handling error with Option: {}", err);
    }

result.is_ok() returns true if the result is an Ok variant and false if it is an Err variant. We can use this method to check if the result is successful before attempting to extract the value.

    if divide(10, 2).is_ok() {
        println!("Division was successful.");
    }

result.is_err() is a method that returns true if the result is an Err variant and false if it is an Ok variant.

    if divide(10, 0).is_err() {
        println!("Division failed.");
    }

Another way to handle errors is to propagate them using the ? operator. The ? operator can be used to return the error to the caller if the result is an Err variant. We essentially say "if this is an error, return it from the current function".

    let final_result = divide_and_multiply(20, 4, 2)?;
    println!("Final Result: {}", final_result);

Unwrapping Results

unwrap() extracts the contents of Ok variant and assigns it to the variable. If the result is an Err variant, it will panic, so use with care. This is typically used in cases where the programmer is certain that the result will be an Ok variant.

    let unwrapped_division = divide(10, 2).unwrap();
    println!("Unwrapped Result: {}", unwrapped_division);

unwrap_or(fallback) will extract the contents of the Ok variant and assign it to the variable. If the result is an Err variant, it will return the default value provided as an argument.

    let value_or_default = divide(10, 2).unwrap_or(0);
    println!("Value with fallback: {}", value_or_default);

unwrap_or_else(fallback_fn) will extract the contents of the Ok variant and assign it to the variable. If the result is an Err variant, it will call the provided closure and return its result. This is useful for providing a computed fallback value based on the error.

    let computed_fallback = divide(10, 0).unwrap_or_else(|err| {
        eprintln!("Error occurred: {}. Providing default value.", err);
        -1
    });
    println!("Computed Fallback: {}", computed_fallback);

expect(message) is similar to unwrap() but allows you to provide a custom error message in case of an Err variant.

    let expected_value = divide(10, 2).expect("Division should succeed");
    println!("Expected Value: {}", expected_value);

Mapping Results

map(convert_fn) allows to change the value in the Result only if it is an Ok variant. The function (usually a closure) is applied to the contents of the Ok variant, and a new Result is returned with the transformed value, if it is an Err variant, it is returned unchanged.

    let result_with_mapped_value = divide(10, 2).map(|val| val * 2);
    println!("Mapped Value Result: {:?}", result_with_mapped_value);

map_err(convert_fn) like map, but it applies the function to the contents of the Err variant instead. This is useful for transforming error types or adding additional context to errors.

    let result_with_mapped_err =
        divide(10, 0).map_err(|e| format!("Custom error: {}", e));
    println!("Mapped Error Result: {:?}", result_with_mapped_err);
    Ok(())
}
$ rustc error-handling.rs
$ ./error-handling
Success: 5
if let success: 5
let else success: 5
Converted to Option: 5
Handling error with Option: Division by zero
Division was successful.
Division failed.
Final Result: 10
Unwrapped Result: 5
Value with fallback: 5
Error occurred: Division by zero. Providing default value.
Computed Fallback: -1
Expected Value: 5
Mapped Value Result: Ok(10)
Mapped Error Result: Err("Custom error: Division by zero")
Go to Index | Next: Ownership