In this post we will review Rust error handling methods and best practice.
The first method to handle errors is to use the panic macro. Notice that once panic was run, there is no standard way to recover from it (putting aside the catch_unwind method). The panic macro will stop the program, and unwind through the call stack, release all memory. An example for panic usage follows below.
extern crate core;
fn end_of(text:String) -> String {
println!("checking the text {text}");
if !text.contains(" "){
panic!("no end of string related work here");
}
let (_,end_of_string) =text.rsplit_once(" ").unwrap();
return String::from(end_of_string);
}
fn main() {
println!("{}", end_of( String::from("Hello World!")));
println!("{}", end_of( String::from("NoSpacesHere")));
}
The output of this example is:
checking the text Hello World!
World!
checking the text NoSpacesHere
thread 'main' panicked at 'no end of string related work here', src/main.rs:7:9
stack backtrace:
0: rust_begin_unwind
at /rustc/84c898d65adf2f39a5a98507f1fe0ce10a2b8dbc/library/std/src/panicking.rs:579:5
1: core::panicking::panic_fmt
at /rustc/84c898d65adf2f39a5a98507f1fe0ce10a2b8dbc/library/core/src/panicking.rs:64:14
2: guessing_game::end_of
at ./src/main.rs:7:9
3: guessing_game::main
at ./src/main.rs:15:19
4: core::ops::function::FnOnce::call_once
at /rustc/84c898d65adf2f39a5a98507f1fe0ce10a2b8dbc/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
Note that we have a detailed stack trace, in addition to the error.
While panic is good for test and examples, in production code we would probably need to avoid terminating our process for each problematic issue. The recommended method is using the Result enum.
extern crate core;
use std::fs::File;
use std::io;
use std::io::Read;
fn end_of(text: String) -> String {
println!("checking the text {text}");
if !text.contains(" ") {
panic!("no end of string related work here");
}
let (_, end_of_string) = text.rsplit_once(" ").unwrap();
return String::from(end_of_string);
}
fn tail_file(file_path: String) -> Result<String, io::Error> {
let my_file = File::open(file_path);
match my_file {
Ok(mut file_handler) => {
let mut file_data = String::new();
let read_result = file_handler.read_to_string(&mut file_data);
return match read_result {
Ok(_) => Ok(end_of(file_data)),
Err(e) => Err(e)
};
}
Err(e) => {
Err(e)
}
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("{}", tail_file(String::from("a.txt"))?);
println!("{}", tail_file(String::from("none existing file"))?);
Ok(())
}
The output of this example is:
checking the text aaa bbb ccc
ccc
Error: Os { code: 2, kind: NotFound, message: "No such file or directory" }
In the example above we've used some of the Result manipulation methods.
First, the tail_file function returns a Result enum typed with String and io::Error.
Second, any call to a function that can return error is following with match arm expression to handle errors.
Third, the main function is using '?' to panic upon errors, but to enable this, we must change the main signature to return Result with a trait.
The tail_file can be also simplified to use the '?' :
fn tail_file(file_path: String) -> Result<String, io::Error> {
let mut file_data = String::new();
File::open(file_path)?.read_to_string(&mut file_data)?;
Ok(end_of(file_data))
}
Notice that in case of error by a result, we are totally blind to the stack trace, which is a very bad practice. To get a full stack of the error from a result, we can choose using the error_stack crate.