One Month Learning Rust - Error Handling

One Month Learning Rust - Error Handling

Daily short news for you
  • People often joke that regex is the language of aliens. Anyone who can write regex has the abilities of an extraterrestrial. It's just a joke, but the truth is that regex is really hard to understand.

    By chance, I came across a website that teaches us regex step by step. During the learning process, we need to interact with dozens of questions ranging from simple to advanced in order to gradually learn how to use this "alien language".

    regexlearn.com

    » Read more
  • Good news to start the day. GitHub has just widely announced GitHub Models to everyone. If you remember, more than 2 months ago, GitHub had a trial program for using LLMs models, and in my case, it took a month to get approved for use. Now, they have given everyone with a GitHub account access, no registration needed anymore 🥳

    GitHub Models is currently a lifesaver for me while building this blog 😆

    GitHub Models is now available in public preview | Github Blog

    » Read more
  • I came across a repository that uses Tauri and Svelte to rewrite an application like the Task Manager on Windows or the Monitor on Mac. I was curious, so I downloaded it and was surprised to find that the app is only a few MB in size and loads quickly. The app itself is also very smooth

    » 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.

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

Hello, my name is Hoai - a developer who tells stories through writing ✍️ and creating products 🚀. With many years of programming experience, I have contributed to various products that bring value to users at my workplace as well as to myself. My hobbies include reading, writing, and researching... I created this blog with the mission of delivering quality articles to the readers of 2coffee.dev.Follow me through these channels LinkedIn, Facebook, Instagram, Telegram.

Did you find this article helpful?
NoYes

Comments (0)

Leave a comment...