Rust: Handling code with Result, Option and panic!

Photo by Shaojie on Unsplash

Rust: Handling code with Result, Option and panic!

·

5 min read

Option

In Rust, Option<T> is used in scenarios where there might not be a result to return. Here, T represents the data type of the data when a value is present. Rust directly exposes the Option type using the Some and None variants. While it's possible, there's no need to use the full Option::Some or Option::None.

#[derive(Debug)]
struct User {
    user_id: i32,
    user_name: String,
}

fn find_user_by_name(name: &str) -> Option<i32> {
    match name {
        // if the name == "art" identify that the data that was requested exists
        // by returning Some with the data found
        "art" => Some(99),
        "pav" => Some(1),
        // catch any other request and return None, identifying that the requested
        // data is not present
        _ => None,
    }
}

fn main() {
    let user_name = "art";

        // try to find the user by name
    let user = find_user_by_name(user_name).map(|id| User {
        user_id: id,
        user_name: user_name.to_owned(),
    });

    match user {
                // if the user is found, print the metadata
        Some(user) => println!(
            "user with name {:?} has the id of {:?}",
            user.user_name, user.user_id
        ),
                // otherwise - inform that the user is not present
        None => println!("user does not exist"),
    }
}

By explicitly representing the possibility of absence using Some and None, Rust ensures that developers handle both cases, thereby reducing the chances of runtime errors related to uninitialized or absent values.

Result

In Rust, Result<T, E> serves as a mechanism to handle functions or operations that might fail. Here, T represents the data type of a successful result, and E denotes the data type of an error. Rust provides the Ok and Err variants directly, making it unnecessary to fully qualify them as Result::Ok or Result::Err.

use std::{fs::File, io::ErrorKind};

fn main() {
    let file_name = "does-not-exist.txt";
    let open_file_result = File::open(file_name);

    let file_instance = match open_file_result {
        Ok(file) => file.metadata(),
        Err(error) => match error.kind() {
            ErrorKind::NotFound => match File::create(file_name) {
                Ok(file) => file.metadata(),
                Err(error) => panic!("failed to create the file {:?}", error),
            },
            other_error => panic!("another error occured {:?}", other_error),
        },
    };

    println!("{:?}", file_instance);
}

Shortcuts for Panic unwrap and expect.

  • If the Result value returns the Ok variant, unwrap will return the value inside the Ok.

  • If the Result is the Err variant, unwrap will call the panic! macro.

Here is an example of unwrap in action:

use std::fs::File;

fn main() {
    let file_instance = File::open("hello.txt").unwrap();
}

Similarly, the expect method not only triggers a panic when encountering an Err, but it also allows specifying a custom panic error message. Here's how expect is used:

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt")
        .expect("hello.txt should be included in this project");
}

Propagating Errors

In Rust, when a function calls an operation that might fail, it's a common pattern not to handle the error directly within the function. Instead, the function can return the error to the caller, allowing it to determine the appropriate course of action. This practice is known as error propagation. It provides the caller, which might have a broader context or more specific information, with the flexibility to decide how to handle potential errors.

Let’s use the example that we used to showcase the Result type.

use std::fs::File;
use std::io::{self, Read};

fn read_file_content() -> Result<String, io::Error> {
    let file_name = "does-not-exist.txt";
    let file_instance_result = File::open(file_name);

    let mut file_result = match file_instance_result {
        Ok(file) => file,
        Err(e) => return Err(e),
    };

    let mut file_content = String::new();

    match file_result.read_to_string(&mut file_content) {
        Ok(_) => Ok(file_content),
        Err(e) => Err(e),
    }
}

fn main() {
    let file_content = read_file_content().unwrap();
    println!("{:?}", file_content);
}

This pattern of propagating errors is so common in Rust that Rust provides the question mark operator ? to make this easier.

Error Propagation Shortcut, question mark operator

In Rust, when dealing with operations that might fail, the ? operator offers a concise way to handle errors. Placing the ? after an expression that returns a Result effectively works as a shorthand for a match expression:

  • If the Result is an Ok, the value within the Ok is extracted and the code continues execution.

  • If the Result is an Err, this error is propagated to the caller, just as if the return keyword was used.

Internally, when the ? operator encounters an error, it leverages the from function from the From trait in the standard library. This function is instrumental in converting between error types, ensuring the error type received matches the one defined in the function's return signature.

Using our previous example, incorporating the ? operator leads to significantly more succinct code:

use std::io;
use std::fs;

fn read_file_content() -> Result<String, io::Error> {
    let file_name = "does-not-exist.txt";
    let mut file_content = String::new();
    File::open(file_name)?.read_to_string(&mut file_content)?;
    Ok(file_content)
    // Alternatively, this can be shortened further using:
    // fs::read_to_string(file_name)
}

It's important to note that the ? operator is versatile and can be applied to both Result and Option types. However, be mindful of type consistency:

While the ? operator can be used with Result in a function returning Result, and similarly for Option, it cannot automatically transform one into the other. In such cases, methods like ok on Result or ok_or on Option can be used for explicit conversions.

Panic vs Result

Many of my articles are inspired by, or at times condensed versions of, The Rust Programming Language. Especially in the initial drafts. For a comprehensive understanding of the "Panic vs. Result" topic, I strongly recommend perusing the relevant chapter from The Rust Book available here.

Reference

Best Text Material

[YouTube] 300 seconds of Rust - 11. Option

Best Video Material

The Rust Programming Language: Error Handling