One Month Learning Rust - Error Handling

One Month Learning Rust - Error Handling

Daily short news for you
  • Since the Lunar New Year holiday has started, I won't be posting anymore. See you all after the holiday! 😁

    » Read more
  • Continuing about jj. I'm wondering if there are any GUI software made for it yet to make it easier to use. There are already so many similar to git that I can't count them all.

    Luckily, the author has compiled them all together in Community-built tools around Jujutsu 🥳

    » Read more
  • Turso announces that they are rewriting SQLite in Rust. This adds another piece of evidence supporting the notion that Rust is "redefining" many things.

    But the deeper reason is more interesting. Why are they doing this? Everyone knows that SQLite is open source, and anyone can create a fork to modify it as they wish. Does the Turso team dislike or distrust C—the language used to build SQLite?

    Let me share a bit of a story. Turso is a provider of database server services based on SQLite; they have made some customizations to a fork of SQLite to serve their purposes, calling it libSQL. They are "generous" in allowing the community to contribute freely.

    Returning to the point that SQLite is open source but not open contribution. There is only a small group of people behind the maintenance of this source code, and they do not accept pull requests from others. This means that any changes or features are created solely by this group. It seems that SQLite is very popular, but the community cannot do what they want, which is to contribute to its development.

    We know that most open source applications usually come with a "tests" directory that contains very strict tests. This makes collaboration in development much easier. If you want to modify or add a new feature, you first need to ensure that the changes pass all the tests. Many reports suggest that SQLite does not publicly share this testing suite. This inadvertently makes it difficult for those who want to modify the source code, as they are uncertain whether their new implementation is compatible with the existing features.

    tursodatabase/limbo is the project rewriting SQLite in Rust mentioned at the beginning of this article. They claim that it is fully compatible with SQLite and completely open source. Limbo is currently in the final stages of development. Let’s wait and see what the results will be in the future. For a detailed article, visit Introducing Limbo: A complete rewrite of SQLite in Rust.

    » Read more

The Issue

Error handling is an essential step that cannot be overlooked in programming. The quality of a program heavily depends on this aspect. When a feature is correctly handled for errors, it can help programmers mitigate many risks later on.

An error can occur due to various reasons, both objective and subjective. Errors can arise from programmers unintentionally overlooking cases, typographical errors, unforeseen logical overlaps, as well as hardware, software, and network connection errors... This is to illustrate that errors are always waiting for the opportunity to emerge immediately.

In Rust, we have two types of errors: Unrecoverable Errors and Recoverable Errors.

Unrecoverable Errors

Unrecoverable errors refer to situations where, upon encountering this error, Rust will immediately stop the program, clean up the memory, and print an error message along with the exact location where the error occurred.

panic! is a macro that generates an unrecoverable error. When panic! is called, it means we want to stop the program immediately.

Generally, panic errors should be triggered when there is dangerous behavior to the program. For example, accessing a nonexistent property:

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

    v[99];
}

Clearly, index 99 does not exist in v, so the most reasonable action here is for Rust to trigger a panic error for this behavior.

In reality, we can also proactively create a panic error simply by calling panic!.

fn main() {
    panic!("crash and burn");
}

Immediately, the program will stop, and details about the error as well as the location causing the error will be returned in the console.

Compiling panic v0.1.0 (file:///projects/panic)
    Finished dev [unoptimized + debuginfo] target(s) in 0.25s
     Running `target/debug/panic`
thread 'main' panicked at 'crash and burn', src/main.rs:2:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Recoverable Errors

In contrast to unrecoverable errors, which indicate dangerous behavior, recoverable errors refer to errors that have a direction for resolution and do not necessarily require stopping the program. For example, if a user inputs an invalid character, instead of exiting, a message can be displayed asking them to re-enter.

Result is an object that generates recoverable errors. Result is an enum consisting of Ok or Err, representing the success value and the error.

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Let's take an example of opening a file in Rust.

use std::fs::File;

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

    let greeting_file = match greeting_file_result {
        Ok(file) => file,
        Err(error) => panic!("Problem opening the file: {:?}", error),
    };
}

File::open returns a Result; if the file opens successfully, Ok has a value; otherwise, Err contains the returned error information. To determine whether greeting_file_result receives Ok or Err, we need to use match to check and handle it.

Typically, there are many reasons for an error, such as the file not existing, lacking read permissions, or system errors... Understanding this, Rust allows the creation of additional error types, which are used to categorize errors.

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    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 above method of error handling is somewhat verbose; Rust provides a mechanism for more concise code by using unwrap and expect.

  • unwrap returns panic! if there is an error.
  • expect is similar to unwrap, but it allows the user to specify an additional error message.
use std::fs::File;

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

Error Propagation

When creating a function, instead of handling errors directly within the function, Rust has a more common approach of returning a Result object. In this way, the error is passed to the calling function for handling, which also helps make the code easier to maintain by avoiding panic! calls scattered throughout the program.

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

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),
    }
}

As in the example above, read_username_from_file returns a Result.

However, there is still too much code written in the above example; we can see that we are constantly checking for Ok and Err values using match. A bit cumbersome, right? Let's simplify it by using the ? operator.

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

fn read_username_from_file() -> 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)
}

Here, using the ? operator commits to the Ok value; if any point panics, the program will stop with a panic error message.

Premium
Hello

The secret stack of Blog

As a developer, are you curious about the technology secrets or the technical debts of this blog? All secrets will be revealed in the article below. What are you waiting for, click now!

As a developer, are you curious about the technology secrets or the technical debts of this blog? All secrets will be revealed in the article below. What are you waiting for, click now!

View all

Subscribe to receive new article notifications

or
* The summary newsletter is sent every 1-2 weeks, cancel anytime.

Comments (0)

Leave a comment...
Scroll or click to go to the next page