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:
- The first line outputs the location of the panic! macro call and the error message it outputs.
- The second line is a hint, which translates to "run with
RUST_BACKTRACE=1
environment variable to display a backtrace." We will introduce backtrace (backtrace) next.
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<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<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<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<i32, bool> {
if i >= 0 { Ok(i) }
else { Err(false) }
}
fn g(i: i32) -> Result<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<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<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 ```