One Month Learning Rust - Error Handling

One Month Learning Rust - Error Handling

Daily short news for you
  • Continuing to update on the lawsuit between the Deno group and Oracle over the name JavaScript: It seems that Deno is at a disadvantage as the court has dismissed the Deno group's complaint. However, in August, they (Oracle) must be held accountable for each reason, acknowledging or denying the allegations presented by the Deno group in the lawsuit.

    JavaScript™ Trademark Update

    » Read more
  • This time last year, I was probably busy running. This year, I'm overwhelmed with work and have lost interest. But sitting too much has made my belly grow, getting all bloated and gaining weight. Well, I’ll just try to walk every day to relax my muscles and mind a bit 😮‍💨

    The goal is over 8k steps 👌

    » Read more
  • Just a small change on the Node.js homepage has stirred the community. Specifically, when you visit the homepage nodejs.org, you will see a button "Get security support for Node.js 18 and below" right below the "Download" button. What’s notable is that it leads to an external website outside of Node.js, discussing a service that provides security solutions for older Node.js versions, which no longer receive security updates. It even stands out more than the Download button.

    The community has condemned this action, stating that it feels a bit "excessive," and suggested consulting them before making such decisions. On the Node side, they argue that this is appropriate as it is from a very significant sponsoring partner. As of now, the link still exists. Let's wait to see what happens next.

    » 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

5 profound lessons

Every product comes with stories. The success of others is an inspiration for many to follow. 5 lessons learned have changed me forever. How about you? Click now!

Every product comes with stories. The success of others is an inspiration for many to follow. 5 lessons learned have changed me forever. How about you? 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...