One peculiarity of Rust is its Result type. In many other programming languages, you’ll probably use a bool or int to indicate successful function execution. If you’re new to Rust and take a look at Rust code, you’ll probably notice a lot of unwrap() going on, which might be your first encounter with the Result type.
Instead of using a boolean type, it has a special type for indicating success or failure of a function/method. It can hold two different values: Ok and the function’s return values, or Err and the error value. It can also be considered the replacement for exceptions and the corresponding try/catch constructs in other languages.
There are several ways to process a Result.
unwrap
This is probably what you’ll encounter first in Rust code:
let a = some_function().unwrap();
some_function returns a Result type. unwrap returns the value that is „wrapped“ by the Ok value. What happens if an Err is returned instead? The program crashes. So unwrap should only be used when you are either sure that the result is Ok, or if it is indeed your goal to terminate the program if something goes wrong.
expect
expect is similar to unwrap, but it allows for sending an error message to stdout if a result is Err:
let a = some_function().expect("Something went wrong!");
I use this e.g. when creating or opening files so that the user can quickly see what is going wrong with which file. Just keep in mind that, like unwrap, the program is going to terminate on errors.
match
Neither unwrap nor expect properly handle errors. That’s fine when these are errors that are unrecoverable anyways (and a graceful program exit is not required), but if there’s a way to deal with them, match will let you do so. The Rust book provides this example:
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:?}"),
};
}
If the result is Ok, it returns the contents of the file variable, which gets its value from the Ok(file) statement.
I actually think that this isn’t the best example, as it panics (crashes) on Err, which could have been achieved with unwrap or expect as well. There are many cases where the Err path can be dealt with in another way, e.g. with assigning a sensible default value. If you’re unsure what is possible with match, here’s the paragraph in the Rust book.
is_ok()
match is an important part of idiomatic Rust, but it also requires quite a bit of code – you don’t use switch in C++ when an if is sufficient. If you just want to know whether a result is Ok or not (especially for functions that don’t have other return values wrapped by Ok), you can use is_ok():
let a = some_function();
if a.is_ok() {
let b = a.unwrap();
// do stuff with b
}
You can of course add an else branch if you want to handle an error.
unwrap_or…()
There are several variants of this. You can use unwrap_or() and unwrap_or_default() to return default values without having to run through an if statement. unwrap_or_else() is sort of the reverse of is_ok(). It unwraps if the result Ok, but (unlike expect or normal unwrap) allows for error handling, as explained here. So there’s many ways to treat results with concise code.
?
Finally, there’s the ? operator. I haven’t actually used it myself, but it is explained here. It can be used in functions that themselves return a Result. The ? operator propagates the results of functions that are called – so if one function call returns Err, so does the surrounding function, without the need of lots of match or if inside the function.
Using Result in your own code
It’s difficult to let go of habits acquired by coding in other languages, but Result is an important part of idiomatic Rust. So next time you’re tempted to write a function that returns true or false as success indicator, use Result instead. Here’s a function that returns all LAZ files in a folder, wrapped in a Result:
pub fn read_input_laz(path: &String) -> Result<Vec<String>, io::Error>
{
let p = Path::new(path);
if !p.exists() {
return Err(io::Error::new(io::ErrorKind::NotFound, format!("Path not found: {}", path)));
}
let mut laz_files: Vec<String> = Vec::new();
if p.is_file() {
return Err(io::Error::new(io::ErrorKind::NotADirectory, format!("Path is a file: {}", path)));
}
for entry in fs::read_dir(p)? {
let entry = entry?;
let path = entry.path();
if path.is_file()
&& let Some(ext) = path.extension()
&& (ext == "laz" || ext == "LAZ")
&& let Some(name) = path.to_str() {
laz_files.push(name.to_string());
}
}
Ok(laz_files)
}
Result values are also used in tests to indicate whether a test has been executed successfully or not.
mod tests {
use super::*;
use std::io::Error;
#[test]
fn test_something() -> Result<(), Error> {
let test = function_to_test(testvalue);
assert!((testvalue-expected).abs()<1e-6);
Ok(())
}
}