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. In addition, Rust also provides the Option type, which is an enum that represents the presence or absence of a value. The Option type has two variants: Some, which contains a value, and None, which represents the absence of a value.

Run code Copy code
use std::error;
use std::fmt;
use std::io;
use std::num::ParseIntError;
fn main() -> Result<(), io::Error> {

Define a function that returns a Result type with a success variant containing a string or an error variant containing an io::Error.

    fn divide(x: i32, y: i32) -> Result<String, io::Error> {
        if y == 0 {
            return Err(
                io::Error::new(
                    io::ErrorKind::Other, "Division by zero"
                )
            );
        }
        Ok(format!("{}", x / y))
    }

The Option type has two variants: Some, which contains a value, and None, which represents the absence of a value.

    fn divide_option(x: i32, y: i32) -> Option<i32> {
        if y == 0 {
            return None;
        }
        Some(x / y)
    }

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

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

The Option type can be used to handle cases where a function may or may not return a value. Here we call the divide_option function with different values and pattern match on the result to handle the Some and None cases.

    match divide_option(10, 0) {
        Some(result) => println!("Result: {}", result),
        None => println!("Error: Division by zero"),
    }

Using the match statement can become verbose. The ? operator offers us a shorthand way to propagate errors up the call stack, returning them to the caller for handling. It can only be used in functions returning a Result.

    let result = divide(10, 2)?;
    println!("Result: {}", result);

Typically you'll see multiple ? operators chained together in a single expression, or in a function that implements the Result trait.

    fn sum(a: &str, b: &str) -> Result<i32, ParseIntError> {
        let result = a.parse::<i32>()? + b.parse::<i32>()?;
        Ok(result)
    }
    match sum("13", "37") {
        Ok(result) => println!("The sum is: {}", result),
        Err(e) => println!("Error: {}", e),
    }

Result<()> is often used in functions that perform some operation but do not return a meaningful value upon success. value but may fail. The () type represents an empty tuple and is used to signify the absence of a value. Return Ok(()) to indicate success, again () is an empty tuple and signifies the absence of a value.

    fn do_something() -> Result<(), io::Error> {
        Ok(())
    }
    do_something()?;

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

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

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

    let result = divide(10, 2).expect("Division by zero");
    println!("Result: {}", result);

unwrap_or() is a method that 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 result = divide(10, 0).unwrap_or(
        "Error: Division by zero".to_string()
    );
    println!("{}", result);

We can declare a custom error types by defining a struct that implements the std::error::Error trait. The Error trait requires the Display trait to be implemented for the custom error type.

    #[derive(Debug)]
    struct CustomError {
        message: String,
    }
    impl fmt::Display for CustomError {
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
            write!(f, "{}", self.message)
        }
    }
    impl error::Error for CustomError {}
    #[derive(Debug)]
    struct AnotherCustomError;
    impl fmt::Display for AnotherCustomError {
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
            write!(f, "Another custom error")
        }
    }
    impl error::Error for AnotherCustomError {}

We can define functions that return custom error types by returning Result<T, E> where E is the custom error type.

    fn f1(i: i32) -> Result<i32, CustomError> {
        if i < 0 {
            return Err(CustomError {
                message: "Custom error".to_string(),
            });
        }
        Ok(i)
    }

We can define multiple custom error types and return them from functions. Here we define another custom error type AnotherCustomError and return it from a function.

    fn f2(i: i32) -> Result<i32, AnotherCustomError> {
        if i > 0 {
            return Err(AnotherCustomError);
        }
        Ok(i)
    }

When a function encounters multiple error types, such as a combination of Option<T> and Result<T, E> or different Result<T, E> types, we can use Box<dyn Error> to handle them uniformly. Box<dyn Error> is a trait object that can represent any type implementing the Error trait, allowing us to return different error types from the same function. Important to note is that we lose the specific error type information when using Box<dyn Error>.

    fn f(i: i32) -> Result<i32, Box<dyn error::Error>> {
        let result = f1(i)?;
        let result = f2(result)?;
        Ok(result)
    }
    f(10).unwrap();

An alternative is to wrap the error type in a custom enum that implements the Error trait. This allows us to define a single error type that can represent multiple error types.

    #[derive(Debug)]
    enum CustomErrorEnum {
        Custom(CustomError),
        Another(AnotherCustomError),
    }
    impl fmt::Display for CustomErrorEnum {
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
            match self {
                CustomErrorEnum::Custom(e) => write!(
                    f, "{}", e,
                ),
                CustomErrorEnum::Another(e) => write!(
                    f, "{}", e,
                ),
            }
        }
    }
    impl error::Error for CustomErrorEnum {}

We can define functions that return custom error types by returning Result<T, E> where E is the custom error type.

    fn g(i: i32) -> Result<i32, CustomErrorEnum> {
        if i < 0 {
            return Err(CustomErrorEnum::Custom(
                CustomError{message: "Custom error".to_string()}
            ));
        } else if i > 100 {
            return Err(CustomErrorEnum::Another(
                AnotherCustomError
            ));
        }
        Ok(i)
    }
    g(10).unwrap();
    Ok(())
}
$ rustc error-handling.rs
$ ./error-handling
Result: 5
Error: Division by zero
Result: 5
The sum is: 50
Result: 5
Result: 5
Error: Division by zero
Go to Index | Next: Ownership