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 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
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");
}
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.
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.
Comments (0)