In Rust we have two types of errors: recoverable and not recoverable. The second one simply occurs when the program propagates not handled error or tries to violate for instance: the memory or someone invoked panic! macro with an error message. As a result program basically terminates. However, we focus in this article on recoverable error, because it is more interesting how to deal with them.
How to propagate errors in Results?
The result is a common concept in rust to achieve error or value at a higher level, like in invoked function capable of returning result scope.
Return contextual result as Err or Ok
A simple way to make aware of higher context about the error is just returning an Error as a result. We can achieve it by invoking Err() function with a string or struct instance argument and returning it afterward.
// Example: We must have file to continue
fn save_new_dictonary_word(dict_path: &Path, word: &str) -> Result<(), Error> {
if !dict_path.exists() {
return Err(Error("Something went wrong"));
}
Ok(())
}
As you can see we can return conditional results depending on the situation. Error type must be correctly specified, it can be exact same type or mapped custom error using trait-from construction written below.
Use ? operator to pass function's or custom error higher
In rust we have a special operator to use end the end of function invocation expression. The purpose of this is to pass errors from invoked context to the top of the current context.
use serde::{Serialize, Deserialize};
use std::io;
use std::path::{Path};
#[derive(Serialize, Deserialize)]
pub struct Receipt {}
pub fn save_receipt(file_loc: &Path, obj: Receipt) -> Result<(), Error> {
let serialized_content = serde_json::to_string(&file_loc)?;
fs::write(&file_loc, serialized_content)?;
}
Map error
We can map our errors with some techniques. The first is about just quickly mapping errors, useful especially when we need to write simple error messages in case of intent to refactor it later if we want. Let's show it by using the previous example :).
pub fn save_receipt(file_loc: &Path, obj: Receipt) -> Result<(), String> {
let serialized_content = serde_json::to_string(&file_loc).map_err(|_| return "Cannot serialize Receipt")?;
fs::write(&file_loc, serialized_content).map_err(|_| return "Cannot save receipt")?;
}
As you can see we can simply use our custom error messages instead of more detailed Error structs when we don't need them. We can also return our custom struct but the type of error must remain compatible. The second way to map foreign errors to our custom one is shown below.
#[derive(Debug, Clone)]
pub struct AccountNumberValidationError;
// Implement default report mechanism
impl fmt::Display for AccountNumberValidationError {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { write!(f, "Cannot save result on disk") }
}
// Create unique type, that will be aossiciated with other added errors to enum body
pub enum CustomError {
InvalidAccountNumber(AccountNumberValidationError),
ResltCannotBeSavedToDisk(io:Error)
}
// Not implement possibility to create CustomError from AccountNumberValidationError
impl From<AccountNumberValidationError> for CustomError {
fn from(value: io::Error) -> Self { Self::InvalidAccountNumber(value) }
}
impl From<io:Error> for CustomError {
fn from(value: io::Error) -> Self { Self::ResltCannotBeSavedToDisk(value) }
}
We can use CustomError in Result like Result<(), CustomError> and the type will be compatible with io:Error and our custom error - AccountNumberValidationError. The purpose of the given example is the possibility to map for instance infrastructure error to application error. So real use case is a isolation between architecture layers.
How to handle errors of expression?
We can handle the error of just one expression in several ways. The first one is very simple, we can just check if the result is an error.
let file_meta_result = fs::symlink_metadata(file_path);
if file_meta_result.is_err() {
// Error handle logic here
}
// Continue when result value can be obtained without raising errors
let file_meta = file_meta_result.unwrap();
I think this example is simple and understandable. A handly approach can be unpacking error from the result in if let statement.
let file_meta_result = fs::symlink_metadata(file_path);
if let Err(error_while_fetching_meta) = file_meta_result {
// Error handle logic here
}
The next example is most advisable to use in ready-production code.
let file_meta = match fs::symlink_metadata(file_path) {
Ok(r) => r, // We can do some transformation on our result before returning it or keep it :)
Err(e) => return e // We can return this error or our custom error in current function :)
};
The interesting thing here is that we can return result from our current function directly from match statement :) . So in case of a positive result, we'll have an unwrapped value but otherwise function will be stopped with an error result. Alternate solutions are just unwrapping the result value when we're sure an error cannot occur in our situation or we can unwrap with the default value when we don't care about errors.
let file_meta_result = fs::symlink_metadata(file_path);
file_meta_result.unwrap(); // We don't care about errors so just unwrap it
file_meta_result.unwrap_or(&default_val); // Unwrap with default value when error occured
I want also to mention about expect method but as fact to avoid this practise, because it is discouraged. It just simply brings panic! unrecoverable error with our custom message.
let file_meta_result = fs::symlink_metadata(file_path).expect("Cannot fetch metadata"); // When result is an error we have unrecoverable error like: "thread 'main' panicked at 'Cannot fetch metadata'"
Worth to know is also that, we can "catch" unrecoverable errors without closing the program as an edge case. By saying "catch" I mean simple converting it to err result.
let result = panic::catch_unwind(|| {
panic!("Your message");
});
assert!(result.is_err());
How to handle errors in case block of instructions?
The best advice here is simple, try to use functions as error boundaries as long as you can. But we have also some alternative ways to manage it, so I want to show you a few examples :). Firstly we can use closure.
let units: Vec<int> = units_bson.iter().map(|u| {
let name = u.as_document().unwrap().get_str("name").map_err(Self::map_mongodb_err)?;
Ok(ItemUnit { name, multiplier, p_buy })
}).collect()?; 1
We have also try block feature for it. Perfect example you can check in offical documentation: https://doc.rust-lang.org/beta/unstable-book/language-features/try-blocks.html.
Summary
Thank you for reading this article. I hope you found what you expected and enjoyed the content. More articles about rust will appear in future so if you're interested in it, please check my activity channels :).