Returning results
Let's take a look at how you return results in Rust. Just to recap, in Rust, the way functions return values is a bit
different from other languages. In Rust, the last expression in a function is the return value. This means that you
don't need to use the return
keyword to return a value from a function. The last expression in the function is
automatically returned.
fn add(a: i32, b: i32) -> i32 { a + b } fn main() { let result = add(1, 2); println!("The result is: {result}"); }
As seen in the previous example, the last expression in the add
function is a + b
, which is automatically returned
from the function. This is a very convenient feature of Rust.
Note that there is no
;
at the end of thea + b
expression. This is because the;
would not return the result of the calculation a+b, but would return the unit type()
. This is a common mistake for people coming from other languages. The()
unit type is the only type that can be returned from a function that does not have a return type specified; in C this would bevoid
.
If you combine this with what you've learned in the previous chapter, you can see that you can return the result of a
match
block, an if
statement, or a loop
block. Here's an example showing how to return the result of an if
statement:
fn max(a: i32, b: i32) -> i32 { if a > b { a } else { b } } fn main() { let result = max(1, 2); println!("{result} is the bigger number"); }
Rust also supports the return
keyword. This is especially useful when you want to return early from a function. Here's
an example:
fn max(a: i32, b: i32) -> i32 { if a > b { return a; } b } fn main() { let result = max(1, 2); println!("{result} is the bigger number"); }
The last two examples are equivalent. The return
keyword is often used to make the code more readable. And prevent
nested if
statements.
The Result
type
So far, we have been returning "errors" as well as good results in the same way. This is not very useful, especially for downstream error handling.
In Rust, functions that have a positive and negative path typically return a Result
type . The Result
is an
enumeration, holding the positive result in Ok()
and the negative result (error) in Err()
.This is referred to
as a "recoverable error" type.
Let's rewrite the previous example to introduce this concept.
fn age_group(age: i32) -> Result<String, String> { let valid_age = match age { _ if age < 0 => return Err("not born yet".to_string()), _ if age > 150 => return Err("seriously?!!".to_string()), validated => validated, }; let result = match valid_age { _ if age < 10 => "child".to_string(), _ if age >= 18 => "adult".to_string(), a => format!("teenager of {} years old", a), }; Ok(result) } fn main() { let age = 15; match age_group(age) { Ok(description) => println!("{}", description), Err(err) => println!("Error: {}", err), } }
This example introduces a few new concepts. First we have the Result
type itself. As you can see it is defined in this
way: Result<T,E>
, where T = the positive result type, and E = the error result type. There are also a few short-hands
in the code to facilitate the use of Result
:
Ok(...)
Err(...)
So when returning a Result
we should use one of these shorthands to indicate if the Result is 'Ok' or an 'Err'.
On the receiving end, we can use a match
block to easily separate the Ok
and Err
responses.
There may be situations where an unrecoverable error occurs. These are situations that cannot be handled downstream and have only one remedy: stop execution.
You can use the panic!
statement in these situations. It is used similar to the println!
operation that we've used
extensively so far.
fn age_group(age: i32) -> Result<String, String> { if age < 0 || age > 150 { panic!("age is out of range"); } let result = match age { _ if age < 10 => "child".to_string(), _ if age >= 18 => "adult".to_string(), a => format!("teenager of {} years old", a), }; Ok(result) } fn main() { let age = -1; match age_group(age) { Ok(description) => println!("{}", description), Err(err) => println!("Error: {}", err), } }
Check the output of the above example
Type alias
If you are using the Result<String, String>
result-type throughout your code, it makes sense to create a type alias
for this result combination. This is done with the type
operator like this:
type AgeResult = Result<String, String>; fn age_group(age: i32) -> AgeResult { if age < 0 || age > 150 { panic!("age is out of range"); } let result = match age { _ if age < 10 => "child".to_string(), _ if age >= 18 => "adult".to_string(), a => format!("teenager of {} years old", a), }; Ok(result) } fn main() { let age = 20; match age_group(age) { Ok(description) => println!("{}", description), Err(err) => println!("Error: {}", err), } }
The ?
operator
Rust provides a very convenient operator to test if an operation succeeded, if so, capture the positive result, if not, return out of the function with the error. This is especially useful when you are running a sequence of operations that could fail as part of a function. Like so:
fn too_young(age: i32) -> Result<i32, String> { if age < 0 { Err("too young".to_string()) } else { Ok(age) } } fn too_old(age: i32) -> Result<i32, String> { if age > 150 { Err("too old".to_string()) } else { Ok(age) } } fn check_age(age: i32) -> Result<i32, String> { let age = too_young(age)?; let age = too_old(age)?; Ok(age) } fn age_group(age: i32) -> Result<String, String> { let validated_age = check_age(age)?; let result = match validated_age { _ if age < 10 => "child".to_string(), _ if age >= 18 => "adult".to_string(), a => format!("teenager of {} years old", a), }; Ok(result) } fn main() { let age = 200; match age_group(age) { Ok(description) => println!("{}", description), Err(err) => println!("Error: {}", err), } }
The
?
operator can be used as long as the error signatures of the functions match.
if let
statement
If you are only interested in the positive result, you can create a conditional path to handle the positive case. Or in reverse, you can create a path that takes care of the error situation and let the positive case pass through. Here's an example:
fn age_group(age: i32) -> Result<String, String> { let valid_age = match age { _ if age < 0 => return Err("not born yet".to_string()), _ if age > 150 => return Err("seriously?!!".to_string()), validated => validated, }; let result = match valid_age { _ if age < 10 => "child".to_string(), _ if age >= 18 => "adult".to_string(), a => format!("teenager of {} years old", a), }; Ok(result) } fn main() { let age = 15; let age_result = age_group(age); if let Ok(description) = age_result { println!("{}", description); } }
Now check what happens if you add the negative path as well:
fn age_group(age: i32) -> Result<String, String> { let valid_age = match age { _ if age < 0 => return Err("not born yet".to_string()), _ if age > 150 => return Err("seriously?!!".to_string()), validated => validated, }; let result = match valid_age { _ if age < 10 => "child".to_string(), _ if age >= 18 => "adult".to_string(), a => format!("teenager of {} years old", a), }; Ok(result) } fn main() { let age = 15; let age_result = age_group(age); if let Ok(description) = age_result { println!("{}", description); } if let Err(err) = age_result { println!("Error: {}", err); } }
We see a familiar error:
error[E0382]: use of moved value: `age_result`
--> src/main.rs:25:23
|
21 | if let Ok(description) = age_result {
| ----------- value moved here
...
25 | if let Err(err) = age_result {
| ^^^^^^^^^^ value used here after partial move
|
= note: move occurs because value has type `std::string::String`, which does not implement the `Copy` trait
What happened?
When we execute the if let
statement, we are actually moving part of the Result
into the description
variable, which moves the ownership with it. This means that age_result
is no longer available after the if let
statement. Often you can borrow the Result
in these situations:
fn age_group(age: i32) -> Result<String, String> { let valid_age = match age { _ if age < 0 => return Err("not born yet".to_string()), _ if age > 150 => return Err("seriously?!!".to_string()), validated => validated, }; let result = match valid_age { _ if age < 10 => "child".to_string(), _ if age >= 18 => "adult".to_string(), a => format!("teenager of {} years old", a), }; Ok(result) } fn main() { let age = 15; let age_result = age_group(age); if let Ok(description) = &age_result { println!("{}", description); } if let Err(err) = &age_result { println!("Error: {}", err); } }