Easy Tutorial
❮ Rust Setup Rust Generics ❯

Error Handling

Rust has a unique mechanism for handling exceptional situations, which is not as straightforward as the try mechanism found in other languages.

Firstly, there are generally two types of errors that can occur in a program: recoverable errors and unrecoverable errors.

A typical example of a recoverable error is a file access error. If accessing a file fails, it might be because the file is currently in use, which is normal, and we can resolve it by waiting.

However, there are also errors caused by logical mistakes in programming that cannot be resolved, such as accessing a position outside the end of an array.

Most programming languages do not distinguish between these two types of errors and use the Exception class to represent them. Rust does not have Exceptions.

For recoverable errors, the Result<T, E> class is used, and for unrecoverable errors, the panic! macro is used.

Unrecoverable Errors

Previously in this chapter, we did not specifically introduce the syntax of Rust macros, but we have already used the println! macro. Since these macros are relatively simple to use, we do not need to fully grasp them yet. We can learn to use the panic! macro in the same way.

Example

fn main() {
    panic!("error occurred");
    println!("Hello, Rust");
}

Running result:

thread 'main' panicked at 'error occurred', src\main.rs:3:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

Clearly, the program does not run as expected to println!("Hello, Rust"), but instead stops running when the panic! macro is called.

Unrecoverable errors will inevitably cause the program to terminate due to fatal damage.

Let's focus on the two lines of error output:

Following the previous example, we create a new terminal in VSCode:

In the new terminal, set the environment variable (different methods for different terminals, here we introduce two main methods):

If you are using Windows 7 or later, the default terminal command line is Powershell, use the following command:

$env:RUST_BACKTRACE=1 ; cargo run

If you are using Linux or macOS, etc., the default terminal command line is usually bash, use the following command:

RUST_BACKTRACE=1 cargo run

Then, you will see the following text:

thread 'main' panicked at 'error occurred', src\main.rs:3:5
stack backtrace:
  ...
  11: greeting::main
             at .\src\main.rs:3
  ...

Backtrace is another way to handle unrecoverable errors. It unfolds the running stack and outputs all information, after which the program still exits. The ellipsis omits a large amount of output information, and we can find the error triggered by our panic! macro.

Recoverable Errors

This concept is very similar to exceptions in the Java programming language. In fact, in C, we often set the return value of a function to an integer to express the errors encountered by the function. In Rust, we use the Result<T, E> enum class as the return value to express exceptions:

enum Result&lt;T, E> {
    Ok(T),
    Err(E),
}

In the Rust standard library, functions that may produce exceptions return a Result type. For example, when we try to open a file:

Example

use std::fs::File;

fn main() {
    let f = File::open("hello.txt");
    match f {
        Ok(file) => {
            println!("File opened successfully.");
        },
        Err(err) => {
            println!("Failed to open the file.");
        }
    }
}

If the hello.txt file does not exist, it will print "Failed to open the file."

Of course, the if let syntax we mentioned in the enum class section can simplify the match syntax block:

Example

use std::fs::File;

fn main() {
    let f = File::open("hello.txt");
    if let Ok(file) = f {
        println!("File opened successfully.");
    } else {
        println!("Failed to open the file.");
    }
}
if file.is_open() {
    println!("File opened successfully.");
} else {
    println!("Failed to open the file.");
}

If you want to treat a recoverable error as an unrecoverable one, the Result type provides two methods: unwrap() and expect(message: &str):

Example

use std::fs::File;

fn main() {
    let f1 = File::open("hello.txt").unwrap();
    let f2 = File::open("hello.txt").expect("Failed to open.");
}

This program is equivalent to calling the panic! macro when the Result is Err. The difference is that expect allows you to send a specified error message to the panic! macro.

Propagating Recoverable Errors

So far, we've discussed how to handle errors when they are received, but what if we want to propagate an error when we encounter one in our own function?

Example

fn f(i: i32) -> Result&lt;i32, bool> {
    if i >= 0 { Ok(i) }
    else { Err(false) }
}

fn main() {
    let r = f(10000);
    if let Ok(v) = r {
        println!("Ok: f(-1) = {}", v);
    } else {
        println!("Err");
    }
}

Output:

Ok: f(-1) = 10000

In this program, the function f is the source of the error. Now, let's write a function g that propagates the error:

Example

fn g(i: i32) -> Result&lt;i32, bool> {
    let t = f(i);
    return match t {
        Ok(i) => Ok(i),
        Err(b) => Err(b)
    };
}

The function g propagates the error that might be returned by f (here, g is a simple example, and actual functions that propagate errors usually include many other operations).

This can be verbose. In Rust, you can append the ? operator to a Result object to propagate the same type of Err directly:

Example

fn f(i: i32) -> Result&lt;i32, bool> {
    if i >= 0 { Ok(i) }
    else { Err(false) }
}

fn g(i: i32) -> Result&lt;i32, bool> {
    let t = f(i)?;
    Ok(t) // Since t is not Err, t is now of type i32
}

fn main() {
    let r = g(10000);
    if let Ok(v) = r {
        println!("Ok: g(10000) = {}", v);
    } else {
        println!("Err");
    }
}

Output:

Ok: g(10000) = 10000

The ? operator actually extracts the non-error value from the Result, and if there is an error, it returns the Result error. Therefore, the ? operator is only used in functions that return Result&lt;T, E>, where E must match the E type of the Result being processed.


The kind Method

So far, Rust does not have syntax like try blocks that can cause all similar exceptions at any location to be resolved directly, but this does not mean that Rust cannot implement it: we can completely implement try blocks in independent functions, passing all exceptions out for resolution. In fact, this is a well-divided program that should follow the programming method: it should focus on the integrity of independent functions.

However, this requires judging the Err type of Result, and the function to get the Err type is kind().

Example

use std::io;
use std::io::Read;
use std::fs::File;

fn read_text_from_file(path: &str) -> Result&lt;String, io::Error> {
    let mut f = File::open(path)?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}

fn main() {
    let str_file = read_text_from_file("hello.txt");
    match str_file {
        Ok(s) => println!("{}", s),
        Err(e) => println!("Error reading file: {:?}", e),
    }
}

Ok(s) => println!("{}", s), Err(e) => { match e.kind() { io::ErrorKind::NotFound => { println!("No such file"); }, _ => { println!("Cannot read the file"); } } }


Output:

No such file ```

❮ Rust Setup Rust Generics ❯