Back to the basics - returning results

Previously we've used an Option<u8> to return a valid age, or nothing. As you've seen, an Option can either have a Some(value) or None. Sometimes it is more useful, maybe even better, to return a specific value in the positive case, and another value in an error condition.

As an alternative to Option, Rust supports the Result type. Where an Option can only hold a single data type, a Result can return one of two possible data types. It has this signature:

Result< [good_type], [bad_type] >

The good_type and bad_type can be any valid Rust type.

In our case we will use this combination: Result<u8,String>. This returns a u8 in the positive case, and a String in case of an error. The values are specified with either Ok(value) or Err(err_value). In our case we'll use: Ok(u8) or Err(String). We use a match block to analyze the result.

This is what the full example would look like:

use std::str::FromStr;

fn main() {
    println!("How old are you?");
    match read_number() {
        Ok(age) => println!("You are {age} years old."),
        Err(err) => println!("{err}"),
    }
}

fn read_string() -> String {
    let mut input = String::new();
    std::io::stdin()
        .read_line(&mut input)
        .expect("can not read user input");
    input.trim().to_string()
}

fn read_number() -> Result<u8, String> {
    let input = read_string();
    u8::from_str(&input).or(Err("You've entered an invalid age".to_string()))
}

Let's look at the code.

The value in the .or(Err("You've entered an invalid age".to_string())) method at the end of read_number() will be returned in case the from_str conversion failed. When this happens, we are returning Err("You've entered an invalid age".to_string())

As described at the beginning of this chapter, the Err(...) method sets the [bad_type] of the Result. In our case from_str already takes care of setting the Ok(...) in case the conversion was successful.

The match block is very similar to the one we've used in the previous chapter, when we checked the returned Option. Only in this case it has two different arms: Ok(age) and Err(err). Both age and err are local variables that get set with the value from the 'good' or 'bad' response, respectively.

The program still behaves the same as in the previous chapter. However, in this case the error message to be printed is set in the read_number() function, not in main().

Exercise

Rewrite the error message to a more generic "You've entered an invalid number. Please enter a value between 0 and 255."

Now that we are returning a more generic error message, we can use the read_number() function to request more numbers than just age, and print the same error if needed.

Since we are returning the error message from the read_number() function, we can now also distinguish between an invalid input and no input.

use std::str::FromStr;

fn main() {
    println!("How old are you?");
    match read_number() {
        Ok(age) => println!("You are {age} years old."),
        Err(err) => println!("{err}"),
    }
}

fn read_string() -> String {
    let mut input = String::new();
    std::io::stdin()
        .read_line(&mut input)
        .expect("can not read user input");
    input.trim().to_string()
}

fn read_number() -> Result<u8, String> {
    let input = read_string();

    if input.is_empty() {
        Err("You did not enter any data".to_string())
    } else {
        u8::from_str(&input).or(Err("You've entered an invalid number".to_string()))
    }
}

We test input.is_empty() to check if the user did not enter any data. We use this in an if block to differentiate between no input and some input.

Notice the lack of ; in the last five lines. This ensures that the result of the if block is returned by the read_number() function.