My Rust Journey - 6th Nov 2024

This is the 8th post about my journey learning the Rust programming language using the Rust Book. Previous posts include:

Chapter 1: Basics of Rust and Cargo

Chapter 3: Mutability and shadowing, variables and constants, scalar and compound data types, functions, control flow with conditional statements and loops

Chapter 4: Ownership, reference and borrowing, and slice type

Chapter 5: Ownership, reference and borrowing, and slice type

Chapter 6: Enums, Control Flow and Matching

Chapter 7: Packages, Crates and Modules

Chapter 8: Common collections

I am documenting this as I think it is a useful thing to do for people interested in learning Rust from my non-developer perspective.

At this stage, you have already installed Rust on your machine, and you are ready to write and run your first Rust programs.

I am using VS code with the rust-analyzer extension. I am working on an M1-mac.

The following tutorial will cover Chapter 9 of the Rust Book. It is meant to be a summary and used with the book as a complementary source of information.

Rust groups errors into:

Recoverable like a file not found → Report to user and retry operation Unrecoverable → always symptoms of a bug, and the program is always stopped

Rust has a type Result<T, E> for recoverable errors and the panic! macro that stops execution when there is an unrecoverable error.

Unrecoverable errors

See the example below:

fn main() {
    let v = vec![1, 2, 3];

    v[99];
}

The program throws the error message

thread 'main' panicked at src/main.rs:4:6:
index out of bounds: the len is 3, but the index is 99
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

The src/main.rs:4:6 indicates the 4th line and 6th character.

We are attempting to access the 100th element in the vector with only three elements. To avoid buffer overread Rust stops execution and refuses to continue.

The error message also suggests running RUST_BACKTRACE=1 cargo run. A backtrace is a list of all functions that have been called.

Recoverable errors

See the code below.

let greeting_file_result = File::open("hello.txt");

    let greeting_file = match greeting_file_result {
        Ok(file) => file,
        Err(error) => match error.kind {
            ErrorKind::NotFound => {
                match File::create("hello.txt") {
                    Ok(fc) => fc,
                    Err(e) => panic!(
                        "Problem creating the file: {:?}",
                        e
                    ),
                }
            }
            other_error => {
                panic!("Problem opening the file: {:?}", other_error);
            }
        },
    };

The return type of File::open is a Result<T, E>. The Result enum and its Ok and Err variants are brought into scope by the prelude, and there is no need to specify result:: in the code.

When the result is Ok, the code returns the file; when it is Err, it calls the panic! macro.

The type of the Err variant is io::Error, a struct provided by the standard library. The struct has the kind method to get an io::ErrorKind, an enum with variants representing different kinds of errors. The one we are interested in is the ErrorKind::NotFound variant, which indicates that the file we are trying to open does not exist yet.

If the file is not found, we create it. A nested match also takes care of failure in creating the file. If the file exists but there is an error that is not NotFound, the program panics.

The code below is a simplified version using closures and the unwrap_or_else method.

let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
        if error.kind() == ErrorKind::NotFound {
            File::create("hello.txt").unwrap_or_else(|error| {
                panic!("Problem creating the file: {:?}", error);
            })
        } else {
            panic!("Problem opening the file: {:?}", error);
        }
    });

The code does not contain and match expression.

The two lines of code below are shortcuts for panic on error using unwrap and expect.

let greeting_file = File::open("hello.txt").unwrap();

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

Propagating errors

The function below shows how we can propagate errors in Rust.

fn read_username_from_file() -> Result<String, io::Error> {
    let username_file_result = File::open("hello.txt");

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

    let mut username = String::new();

    match username_file.read_to_string(&mut username) {
        Ok(_) => Ok(username),
        Err(e) => Err(e), // no need to call return, it's the last expression!
    }
}

The function returns a value of the type Result<T, E> where the generic parameter T is filled with the String type and the generic parameter E with the io::Error type. The function calls the File::open function and handles the Result value with a match. If the file opening is successful, the file handle becomes the value of the mutable variable username_file. In case of an error in opening the file, we pass the error to the variable e and we return early out of the function.

If we get a file handle within username_file, we create a mutable variable for username and with read_to_string method we read the content in username_file and put it into username. The method also returns a Result that we need to match. If read_to_string succeeds we return the result, if it fails we pass the error to the e variable.

This way of propagating errors is common, and rust uses the ? operator to make the code cleaner.

fn read_username_from_file_2() -> Result<String, io::Error> {
    let mut username_file = File::open("hello.txt")?;
    let mut username = String::new();
    username_file.read_to_string(&mut username)?;
    Ok(username)
}

The function fs::read_to_string opens the file, creates a new String, reads the contents of the file, puts the contents into the string and returns it.

fn read_username_from_file_3() -> Result<String, io::Error> {
    std::fs::read_to_string("hello.txt")
}

The ? operator after a Result value works similarly to the match expressions we defined. The ? operator additionally calls the from function and converts the received error type into the error type defined by the current function.

The ? operator can only be used in functions where the return type is Result or Option. The operator performs an early return of the function.

In this chapter, we learned about how to handle errors in Rust. See you in the next post!

0
filippoweb3Post author

🔥 Web3 explained from the non-developer's POV. 🚀 Helping Polkadot users explore the ecosystem with confidence. I post daily on socials. Opinions are mine.

1 comment

Loading replies...

🔥 Web3 explained from the non-developer's POV. 🚀 Helping Polkadot users explore the ecosystem with confidence. I post... Show More