Your example never really uses the result of any operations so it may be a bit too contrived to show proper error handling in Rust, but I'll give it a shot.
In Rust you usually try to provide more information about an error than just "it failed", so it's quite common to create an error type containing contextual information about what went wrong.
For example:
#[derive(Debug)] // Note: derive automatically generates a way to print this to the screen
enum MyError {
/// An error from the operating system (e.g. file not found)
Io(std::io::Error),
SomethingWentWrong {
msg: String,
},
}
Also, you know how you always need to remember to check the error code before accessing the result of an operation in C? Well Rust uses the type system to make sure it's impossible to access the result without doing the check. This is done using the Result (something which is either a successful outcome with a result, or a failed outcome with an error).
You use pattern matching to deal with either the success or failure, it's analogous to switching on status.
fn fallible_operation() -> Result<u32, MyError > {
unimplemented!()
}
fn do_something_and_log_error() {
match fallible_operation() {
Ok(n) => println!("Got {}", n),
Err(e) => println!("An error occurred: {:?}", e),
}
}
fn add_one_or_propagate_errors_up() -> Result<u32, MyError > {
match fallible_operation() {
Ok(n) => Ok(n + 1)
Err(e) => Err(e),
}
}
Because doing something with the "success" variant from a result is such a common operation, Result has a map() method which will let you transform the success value.
fn add_one_or_propagate_errors_up_with_map() -> Result<u32, MyError > {
fallible_operation().map(|n| n + 1)
}
Another thing is that idiomatic Rust prefers to return early when something goes wrong instead of adding loads of if (!failed) { ... } blocks. One way to do this is with explicit match blocks.
fn add_result_of_two_fallible_operations() -> Result<u32, MyError > {
let first = match fallible_operation() {
Ok(n) => n,
Err(e) => return Err(e),
};
let second = match fallible_operation() {
Ok(n) => n,
Err(e) => return Err(e),
};
Ok(first + second)
}
However, it turns out matching on a result and returning if there is an error is so common that the ? operator was introduced. This lets us clean up add_result_of_two_fallible_operations().
It may seem like a trivial thing, but this killer feature lets you still explicitly handle errors while not cluttering the code with if error { ... } checks.
fn add_result_of_two_fallible_operations() -> Result<u32, MyError > {
let first = fallible_operation()?;
let second = fallible_operation()?;
Ok(first + second)
}
You can even use the ? operator to return an error from main(). If that happens, the _start function automatically generated by the compiler will print an error message and set the return code appropriately.
fn main() -> Result<(), RandomNumberError > {
let a= add_two_random_numbers()?;
let b= add_two_random_numbers()?;
println!("All operation successful");
Ok(())
}
If the error handling logic is non-trivial you'll usually write an explicit match and inspect the error to figure out what went wrong.