Option
In Rust, Option<T>
is used in scenarios where there might not be a result to return. Here, T
represents the data type of the data when a value is present. Rust directly exposes the Option
type using the Some
and None
variants. While it's possible, there's no need to use the full Option::Some
or Option::None
.
#[derive(Debug)]
struct User {
user_id: i32,
user_name: String,
}
fn find_user_by_name(name: &str) -> Option<i32> {
match name {
// if the name == "art" identify that the data that was requested exists
// by returning Some with the data found
"art" => Some(99),
"pav" => Some(1),
// catch any other request and return None, identifying that the requested
// data is not present
_ => None,
}
}
fn main() {
let user_name = "art";
// try to find the user by name
let user = find_user_by_name(user_name).map(|id| User {
user_id: id,
user_name: user_name.to_owned(),
});
match user {
// if the user is found, print the metadata
Some(user) => println!(
"user with name {:?} has the id of {:?}",
user.user_name, user.user_id
),
// otherwise - inform that the user is not present
None => println!("user does not exist"),
}
}
By explicitly representing the possibility of absence using Some
and None
, Rust ensures that developers handle both cases, thereby reducing the chances of runtime errors related to uninitialized or absent values.
Result
In Rust, Result<T, E>
serves as a mechanism to handle functions or operations that might fail. Here, T
represents the data type of a successful result, and E
denotes the data type of an error. Rust provides the Ok
and Err
variants directly, making it unnecessary to fully qualify them as Result::Ok
or Result::Err
.
use std::{fs::File, io::ErrorKind};
fn main() {
let file_name = "does-not-exist.txt";
let open_file_result = File::open(file_name);
let file_instance = match open_file_result {
Ok(file) => file.metadata(),
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create(file_name) {
Ok(file) => file.metadata(),
Err(error) => panic!("failed to create the file {:?}", error),
},
other_error => panic!("another error occured {:?}", other_error),
},
};
println!("{:?}", file_instance);
}
Shortcuts for Panic unwrap
and expect
.
If the
Result
value returns theOk
variant,unwrap
will return the value inside theOk
.If the
Result
is theErr
variant,unwrap
will call thepanic!
macro.
Here is an example of unwrap
in action:
use std::fs::File;
fn main() {
let file_instance = File::open("hello.txt").unwrap();
}
Similarly, the expect
method not only triggers a panic when encountering an Err
, but it also allows specifying a custom panic error message. Here's how expect
is used:
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt")
.expect("hello.txt should be included in this project");
}
Propagating Errors
In Rust, when a function calls an operation that might fail, it's a common pattern not to handle the error directly within the function. Instead, the function can return the error to the caller, allowing it to determine the appropriate course of action. This practice is known as error propagation. It provides the caller, which might have a broader context or more specific information, with the flexibility to decide how to handle potential errors.
Let’s use the example that we used to showcase the Result
type.
use std::fs::File;
use std::io::{self, Read};
fn read_file_content() -> Result<String, io::Error> {
let file_name = "does-not-exist.txt";
let file_instance_result = File::open(file_name);
let mut file_result = match file_instance_result {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut file_content = String::new();
match file_result.read_to_string(&mut file_content) {
Ok(_) => Ok(file_content),
Err(e) => Err(e),
}
}
fn main() {
let file_content = read_file_content().unwrap();
println!("{:?}", file_content);
}
This pattern of propagating errors is so common in Rust that Rust provides the question mark operator ?
to make this easier.
Error Propagation Shortcut, question mark operator
In Rust, when dealing with operations that might fail, the ?
operator offers a concise way to handle errors. Placing the ?
after an expression that returns a Result
effectively works as a shorthand for a match expression:
If the
Result
is anOk
, the value within theOk
is extracted and the code continues execution.If the
Result
is anErr
, this error is propagated to the caller, just as if thereturn
keyword was used.
Internally, when the ?
operator encounters an error, it leverages the from
function from the From
trait in the standard library. This function is instrumental in converting between error types, ensuring the error type received matches the one defined in the function's return signature.
Using our previous example, incorporating the ?
operator leads to significantly more succinct code:
use std::io;
use std::fs;
fn read_file_content() -> Result<String, io::Error> {
let file_name = "does-not-exist.txt";
let mut file_content = String::new();
File::open(file_name)?.read_to_string(&mut file_content)?;
Ok(file_content)
// Alternatively, this can be shortened further using:
// fs::read_to_string(file_name)
}
It's important to note that the ?
operator is versatile and can be applied to both Result
and Option
types. However, be mindful of type consistency:
While the ?
operator can be used with Result in a function returning Result, and similarly for Option, it cannot automatically transform one into the other. In such cases, methods like ok
on Result
or ok_or
on Option
can be used for explicit conversions.
Panic vs Result
Many of my articles are inspired by, or at times condensed versions of, The Rust Programming Language. Especially in the initial drafts. For a comprehensive understanding of the "Panic vs. Result" topic, I strongly recommend perusing the relevant chapter from The Rust Book available here.
Reference
Best Text Material
[YouTube] 300 seconds of Rust - 11. Option