An Result is a type for representing success or failure in an operation.
In the article about panic! we introduced Result, and in the previous one we saw Option for absent values. Now we are going to dive deeper into Result to design our own functions that can fail, use type aliases, and manipulate errors in a functional style.
We already know that in Rust, errors are values. Specifically, they are variants of the Result<T, E> enum:
enum Result<T, E> {
Ok(T), // Everything went well, here's the data.
Err(E), // Something failed, here's the reason.
}So far we have been consumers of Result (using standard library functions like File::open). Today we are going to create our own results and transform them without losing error information.
Writing functions that can fail
Imagine we want to create a function for division. Mathematically, dividing by zero is undefined. In C++ or Java, you would throw an exception. In C, you would return a weird error code or NaN.
In Rust, we return a Result.
fn divide(numerator: f64, denominator: f64) -> Result<f64, String> {
if denominator == 0.0 {
// Build the error by wrapping it in Err
return Err(String::from("Cannot divide by zero"));
}
// Return the success wrapped in Ok
Ok(numerator / denominator)
}
fn main() {
let result = divide(10.0, 0.0);
match result {
Ok(value) => println!("Result: {}", value),
Err(message) => println!("Error: {}", message),
}
}Notice the signature: Result<f64, String>.
T(Success) isf64.E(Error) isString(a simple text message).
Type aliases
If you write a lot of code using the standard library std::io (input/output), you will see many functions return io::Result<T>. Where is the E?
Rust allows you to create Type Aliases to avoid repeating code. The standard library does this to save you from writing the error type over and over. In the io module, the error is always std::io::Error.
You can do the same in your own modules:
// Define a specific alias for our math logic
type MathResult<T> = Result<T, String>;
// Now the signature is much cleaner
fn divide(a: f64, b: f64) -> MathResult<f64> {
if b == 0.0 {
return Err(String::from("Division by zero"));
}
Ok(a / b)
}Transforming successes and errors
match is very expressive, but sometimes verbose. Rust offers a series of combinators (functional methods) inspired by Option, but adapted to handle success or failure without unpacking the box.
map: transform the success
Imagine you call a function that returns Result<i32, String>. If it works, you want to multiply the number by 10. If it fails, you want to leave the error as is.
let result: Result<i32, String> = Ok(5);
// If it's Ok(5) -> it becomes Ok("50")
// If it were Err("bad") -> it would stay Err("bad")
let text = result.map(|n| (n * 10).to_string());map_err: transform the error
This is very useful when working with different libraries. Sometimes we receive a “Database” error but our function promises to return an “API” error. We need to transform the E.
fn process_data() -> Result<i32, String> {
let value = function_that_fails().map_err(|e| {
format!("Internal library error: {}", e)
})?; // The ? operator now propagates our new String error
Ok(value)
}unwrap_or and unwrap_or_else
Just like with Option, we can provide fallback values in case of error.
let config = read_config().unwrap_or_else(|_| Config::default());This is very useful for tolerating non-critical failures.
From Option to Result and vice versa
Sometimes you have an Option (something that might not exist) but your function needs to return a Result (an explanatory error).
The method .ok_or(error) transforms an Option into a Result.
fn find_user(id: i32) -> Option<String> {
if id == 1 { Some("Luis".to_string()) } else { None }
}
fn login_user(id: i32) -> Result<(), String> {
// Transform the None into an Err("User not found")
let user = find_user(id).ok_or("User not found")?;
println!("User logged in: {}", user);
Ok(())
}There is also the reverse: .ok() transforms a Result into an Option, discarding the error information (converting Err into None).
let file = File::open("does_not_exist.txt").ok(); // Option<File> is NoneBest practices: Don’t use String for errors
In the examples above I used String as the error type (Result<T, String>) for simplicity.
However, in real code (especially libraries), this is bad practice.
- A
Stringusually requires a memory allocation. - Strings are not easy to handle programmatically (you can’t do
match erroreasily to distinguish a “Permission denied” error from a “Disk full” one).
The correct way is to use an Enum for your errors, or use standard error types.
#[derive(Debug)]
enum MathError {
DivisionByZero,
NegativeSquareRoot,
Overflow,
}
fn divide(a: f64, b: f64) -> Result<f64, MathError> {
if b == 0.0 {
return Err(MathError::DivisionByZero);
}
Ok(a / b)
}This allows the user of your function to react specifically:
match divide(10.0, 0.0) {
Ok(val) => println!("{}", val),
Err(MathError::DivisionByZero) => println!("Watch out for zero!"),
Err(_) => println!("Other math error"),
}