6 More collections, more error handling

This chapter covers

Rust has a lot more collection types than the ones we learned in chapter 3. You might not need all of them right away, but be sure to give each collection type a read so that you’ll remember when you might need each one of them. This chapter also introduces one of Rust’s most loved operators: ? (Yes, it’s just a question mark.)

6.1 Other collections

Rust has many more types of collections besides the ones we learned in chapter 3. All of them are contained in the same spot: the std::collections module in the standard library. The best way to use them is to bring them into scope with a use statement, like we did with our enums in the last chapter. The page for the collections module (http://mng.bz/27yd) on the standard library has a really nice summary of when to use which collection type and for what reasons, so be sure to bookmark it.

We will start with HashMap, which is extremely common.

6.1.1 HashMap (and BTreeMap)

A HashMap is a collection made out of keys and values. You use the key to look up the value that matches the key. An example of a key and a value is email and my_email@ .address.com (email is the key; the address is the value).

Creating a new HashMap is easy: you can just use HashMap::new(). After that, you can use the .insert(key, value) method to insert items.

The keys of a HashMap are not ordered, so if you print every key in a HashMap together it will probably print differently. We can see this in an example:

use std::collections::HashMap;                         
 
struct City {
    name: String,
    population: HashMap<i32, i32>,                     
}
 
fn main() {
 
    let mut tallinn = City {
        name: "Tallinn".to_string(),
        population: HashMap::new(),                    
    };
 
    tallinn.population.insert(2020, 437_619);          
    tallinn.population.insert(1372, 3_250);
    tallinn.population.insert(1851, 24_000);           
 
    for (year, population) in tallinn.population {     
        println!("In {year}, Tallinn had a population of {population}.");
    }
}

This is so we can just write HashMap instead of std::collections::HashMap every time.

This will have the year and the population for the year.

So far the HashMap is empty.

Inserts three dates

Just so we remember, there is no difference between 24_000 and 24000. The _ is just for readability.

The HashMap is HashMap<i32, i32>, so it returns two items each time.

Here the three keys are 2020, 1372, and 1851. If a HashMap were ordered, the order would be 1372, 1851, and 2020. But because a HashMap does not order its keys, we will see them in any order. So, the code might print

In 1372, Tallinn had a population of 3250.
In 2020, Tallinn had a population of 437619.
In 1851, Tallinn had a population of 24000.

or it might print

In 1851, Tallinn had a population of 24000.
In 2020, Tallinn had a population of 437619.
In 1372, Tallinn had a population of 3250.

You can see that the keys do not appear in any particular order.

If you want a HashMap that gives you its keys in order, you can use a BTreeMap. Underneath, they are different types, but fortunately, their method names and signatures are very similar. That means we can quickly change our HashMap to a BTreeMap without needing to change almost anything. For our simple example, the code (besides the name BTreeMap) hasn’t changed at all:

use std::collections::BTreeMap;       
 
struct City {
    name: String,
    population: BTreeMap<i32, i32>,   
}
 
fn main() {
 
    let mut tallinn = City {
        name: "Tallinn".to_string(),
        population: BTreeMap::new(),  
    };
 
    tallinn.population.insert(2020, 437_619);
    tallinn.population.insert(1372, 3_250);
    tallinn.population.insert(1851, 24_000);
 
    for (year, population) in tallinn.population {
        println!("In {year}, Tallinn had a population of {population}.");
    }
}

Changes HashMap to BTreeMap

Here, too

And here, too

Now, it will always print in this order:

In 1372, Tallinn had a population of 3250.
In 1851, Tallinn had a population of 24000.
In 2020, Tallinn had a population of 437619.

Now, we will go back to HashMap.

The simplest but least rigorous way to get a value in a HashMap is by putting the key in [] square brackets, similar to typing [0] or [1] to index a Vec. In this next example, we will use this method to look for the value for the key Bielefeld, which is Germany. But be careful because the program will crash if there is no key, just like when indexing a Vec. If you write println!("{:?}", city_hashmap["Bielefeldd"]);, for example, it will panic because Bielefeldd doesn’t exist.

If you are not sure there will be a key, you can use .get(), which returns an Option. If it exists, it will be Some(value), and if not, you will get None instead of panicking the program. That’s why .get() is the safer way to get a value from a HashMap:

use std::collections::HashMap;
 
fn main() {
    let canadian_cities = vec!["Calgary", "Vancouver", "Gimli"];
    let german_cities = vec!["Karlsruhe", "Bad Doberan", "Bielefeld"];
 
    let mut city_hashmap = HashMap::new();
 
    for city in canadian_cities {
        city_hashmap.insert(city, "Canada");
    }
    for city in german_cities {
        city_hashmap.insert(city, "Germany");
    }
 
    println!("{:?}", city_hashmap["Bielefeld"]);
    println!("{:?}", city_hashmap.get("Bielefeld"));
    println!("{:?}", city_hashmap.get("Bielefeldd"));
}

This prints

"Germany"
Some("Germany")
None

This is because Bielefeld exists, but Bielefeldd does not exist.

If a HashMap already has a key when you try to put it in, using .insert() will overwrite its value:

use std::collections::HashMap;
 
fn main() {
    let mut book_hashmap = HashMap::new();
 
    book_hashmap.insert(1, "L'Allemagne Moderne");
    book_hashmap.insert(1, "Le Petit Prince");
    book_hashmap.insert(1, "섀도우 오브 유어 스마일");
    book_hashmap.insert(1, "Eye of the World");
 
    println!("{:?}", book_hashmap.get(&1));    
}

The .get() method takes a reference, which is why we have &1 here.

This prints Some("Eye of the World") because it was the last one we used .insert() for. It is easy to prevent this by checking whether an entry exists since .get() returns an Option:

use std::collections::HashMap;
 
fn main() {
    let mut book_hashmap = HashMap::new();
    book_hashmap.insert(1, "L'Allemagne Moderne");
 
    let key = 1;
    match book_hashmap.get(&key) {
        Some(val) => println!("Key {key} has a value already: {val}"),
        None => {
            book_hashmap.insert(key, "Le Petit Prince");
        }
    }
    println!("{:?}", book_hashmap.get(&1));
}

This prints Some("L\'Allemagne Moderne") because there was already a key for 1, so we didn’t insert Le Petit Prince.

You might be wondering why we put book_hashmap.insert() inside a {} but didn’t do the same for the print statement. That’s because .insert()returns a value: an Option that holds the old value if the value was overwritten. And because each arm of a match statement has to return the same type, we can have the part with .insert() return a () by enclosing it in {} and ending it with a semicolon.

Let’s try grabbing the old value from the .insert() method and storing it somewhere else so we don’t lose it. In this next sample, we will have a Vec that will hold any old values that have been returned by the .insert() method when an existing value has been overwritten:

use std::collections::HashMap;
 
fn main() {
    let mut book_hashmap = HashMap::new();
    let mut old_hashmap_values = Vec::new();
 
    let hashmap_entries = [
        (1, "L'Allemagne Moderne"),
        (1, "Le Petit Prince"),
        (1, "섀도우 오브 유어 스마일"),
        (1, "Eye of the World"),
    ];
 
    for (key, value) in hashmap_entries {     
        if let Some(old_value) = book_hashmap.insert(key, value) {
            println!("Overwriting {old_value} with {value}!");
            old_hashmap_values.push(old_value);
        }
    }
    println!("All old values: {old_hashmap_values:?}");
}

Don’t forget to destructure here! You don’t have to, but destructuring into (key, value) is much nicer to work with than something like entry.0 and entry.1.

Here’s what the output looks like:

Overwriting L'Allemagne Moderne with Le Petit Prince!
Overwriting Le Petit Prince with 섀도우 오브 유어 스마일!
Overwriting 섀도우 오브 유어 스마일 with Eye of the World!
All old values: ["L'Allemagne Moderne", "Le Petit Prince", "섀도우 오브 유어 스마일"]

The .entry() method

HashMap has a very interesting method called .entry() that you definitely want to try out. It’s a little complicated, so let’s look at it one bit at a time.

With .entry(), you can try to make an entry and then another method like .or_insert() to insert a default value if there is no key. The interesting part is that the second method also returns a mutable reference, so you can change it if you want. First is an example where we insert true every time we insert a book title into the HashMap.

Let’s pretend that we have a library and want to keep track of our books:

use std::collections::HashMap;
 
fn main() {
    let book_collection = vec![
        "L'Allemagne Moderne",
        "Le Petit Prince",
        "Eye of the World",
        "Eye of the World",       
    ];
 
    let mut book_hashmap = HashMap::new();
 
    for book in book_collection {
        book_hashmap.entry(book).or_insert(true);
    }
    for (book, true_or_false) in book_hashmap {
        println!("Do we have {book}? {true_or_false}");
    }
}

Note that Eye of the World appears twice.

This prints

Do we have Eye of the World? true
Do we have Le Petit Prince? true
Do we have L'Allemagne Moderne? true

This worked, but so far, we’ve only used .entry() and .or_insert() like the .insert() method. Maybe it would be better to count the number of books so that we know that there are two copies of Eye of the World.

Here’s how it works. Let’s look at what .entry() does and then what .or_insert() does. First is .entry(), which only takes a key. It then returns an enum called Entry:

pub fn entry(&mut self, key: K) -> Entry<K, V>

The page for Entry can be found at http://mng.bz/1JXV. Here is a simple version of its code. K means key, and V means value:

enum Entry<K, V> {
    Occupied(OccupiedEntry<K, V>),
    Vacant(VacantEntry<K, V>),
}

So, when you use .entry(), the HashMap will check the key that it got and return an Entry to let you know whether there is a value.

The next method, .or_insert(), is a method on the Entry enum. This method looks at the enum and decides what to do:

fn or_insert(self, default: V) -> &mut V {
    match self {
        Occupied(entry) => entry.into_mut(),
        Vacant(entry) => entry.insert(default),
    }
}

The interesting part is that it returns a mutable reference: &mut V. It either returns a mutable reference to the existing value, or it inserts the default value and then returns a mutable reference to it. In either case, it returns a mutable reference.

That means you can use let to attach the mutable reference to a variable name and change the variable to change the value in the HashMap. So let’s give that a try. For every book, we will insert a default 0 if there is no entry and then get a mutable reference to the value. We will then increase it by 1. That means that inserting the first book will return a 0, which we will increment to 1: one book. If we insert the same book again, it will return a 1, which we will increment to 2: two books. And so on.

Now the code looks like this:

use std::collections::HashMap;
 
fn main() {
    let book_collection = vec![
        "L'Allemagne Moderne",
        "Le Petit Prince",
        "Eye of the World",
        "Eye of the World",
    ];
 
    let mut book_hashmap = HashMap::new();
 
    for book in book_collection {
        let return_value = book_hashmap.entry(book).or_insert(0);   
        *return_value += 1;                                         
    }
 
    for (book, number) in book_hashmap {
        println!("{book}, {number}");
    }
}

The variable return_value is a mutable reference. If nothing is there, it will be 0.

Now return_value is at least 1. And if there was another book, the number it returns will now be increased by 1.

The important part is

let return_value = book_hashmap.entry(book).or_insert(0); 

If you take out the let, you get book_hashmap.entry(book).or_insert(0). Without let, it does nothing: it inserts 0, and no variable holds onto the mutable reference to 0. We bind it to return_value so we can keep the 0. Then we increase the value by 1, which gives at least 1 for every book in the HashMap. Then when .entry() looks at Eye of the World again, it doesn’t insert anything, but it gives us a mutable 1. Then we increase it to 2, and that’s why it prints this:

L'Allemagne Moderne, 1
Le Petit Prince, 1
Eye of the World, 2

You can also do things with .or_insert(), such as insert a Vec and then push a value onto it. Let’s pretend that we asked men and women on the street what they think of a politician. They give a rating from 0 to 10. We want to put the numbers together to see whether the politician is more popular with men or women. It can look like this:

use std::collections::HashMap;
 
fn main() {
    let data = vec![                                                   
        ("male", 9),
        ("female", 5),
        ("male", 0),
        ("female", 6),
        ("female", 5),
        ("male", 10),
    ];
 
    let mut survey_hash = HashMap::new();
 
    for item in data {                                                 
        survey_hash.entry(item.0).or_insert(Vec::new()).push(item.1);  
    }
 
    for (male_or_female, numbers) in survey_hash {
        println!("{male_or_female}: {numbers:?}");
    }
}

This is the raw data.

This gives a tuple of (&str, i32).

Here we push the number into the Vec inside. This is possible because after .or_insert(), we have a mutable reference to the data, which is a Vec<i32>.

This prints

"female", [5, 6, 5]
"male", [9, 0, 10]

Or it might print "male" first—remember, a HashMap is unordered. Here as well you could use the same code with a BTreeMap if you wanted the keys to be ordered.

The important line is

survey_hash.entry(item.0).or_insert(Vec::new()).push(item.1);

So if the HashMap sees the key "female", it will check to see whether this key is already in the HashMap. If not, it will insert a Vec::new() and return a mutable reference to it; then we can use .push() to push the first number in. If it sees "female" already in the HashMap, it will not insert a new Vec, but it will return a mutable reference to that Vec, and then we can push a new number into it.

The next collection type is pretty similar to HashMap (even the name is similar) but simpler!

6.1.2 HashSet and BTreeSet

A HashSet is actually just a HashMap that only has keys. The documentation page for HashSet (https://doc.rust-lang.org/std/collections/struct.HashSet.html) has a pretty simple explanation for this: “implemented as a HashMap where the value is ().” That means that a HashSet is useful as a collection that lets you know whether a key exists or not.

Imagine that you have 50 random numbers, and each number is between 1 and 50. Some numbers will appear more than once, while some won’t appear at all. If you put them into a HashSet, you will have a list of all the numbers that appeared:

use std::collections::HashSet;
 
fn main() {
    let many_numbers = vec![
        37, 3, 25, 11, 27, 3, 37, 21, 36, 19, 37, 30, 48, 28, 16, 33, 2,
        10, 1, 12, 38, 35, 30, 21,
        20, 38, 16, 48, 39, 31, 41, 32, 50, 7, 15, 1, 20, 3, 33, 12, 1, 11,
        34, 38, 49, 1, 27, 9,
        46, 33,
    ];
 
    println!("How many numbers in the Vec? {}", many_numbers.len());
 
    let mut number_hashset = HashSet::new();
 
    for number in many_numbers {
        number_hashset.insert(number);
    }
 
    let hashset_length = number_hashset.len();   
    println!(
        "There are {hashset_length} unique numbers, so we are missing {}.",
        50 - hashset_length
    );
 
    println!("It does not contain: ");           
    for number in 0..=50 {
        if number_hashset.get(&number).is_none() {
            print!("{number} ");
        }
    }
}

Like a Vec, the other collection types have a .len() method, too, that tells you how many items it holds.

Let’s see what numbers we are missing.

This prints

How many numbers in the Vec? 50
There are 31 unique numbers, so we are missing 19.
It does not contain: 
0 4 5 6 8 13 14 17 18 22 23 24 26 29 40 42 43 44 45 47 

A BTreeSet is similar to a HashSet in the same way that a BTreeMap is similar to a HashMap. If we print each item in the HashSet, we don’t know what the order will be:

for entry in number_hashset {
    print!("{} ", entry);
}

Maybe it will print

48, 27, 36, 16, 32, 37, 41, 20, 7, 25, 15, 35, 3, 33, 21, 39, 12,
2, 46, 19, 31, 30, 10, 49, 28, 34, 50, 11, 1, 38, 9. 

But it will almost never print these numbers in the same way again.

Here as well, it is easy to change your HashSet to a BTreeSet if you decide you need ordering. In our code, we only need to make two changes to switch from a HashSet to a BTreeSet.

Instead of just printing out the numbers in a BTreeSet, let’s demonstrate that each number is greater than the last. To do this, we can keep track of the latest number and then compare it to the next number the BTreeSet contains. What do you think the following code will print?

use std::collections::BTreeSet;
 
fn main() {
    let many_numbers = vec![37, 3, 25, 11, 27, 3, 37, 21, 36, 19, 37, 30, 48,
        28, 16, 33, 2, 10, 1, 12, 38, 35, 30, 21, 20, 38, 16, 48, 39, 31, 41,
        32, 50, 7, 15, 1, 20, 3, 33, 12, 1, 11, 34, 38, 49, 1, 27, 9, 46, 33];
 
    let mut current_number = i32::MIN;          
    let mut number_set = BTreeSet::new();
    for number in many_numbers {
        number_set.insert(number);
    }
    for number in number_set {
        if number < current_number {            
            println!("This will never happen");
        }
        current_number = number;                
    }
}

We are going to compare increasingly large numbers, so the best way to start is with a number that is lower than any number in the BTreeSet. We could have gone with -1, but another interesting way is to pick the lowest number possible for an i32.

For each number, we will check to see whether it is less than the last number. That will never happen, though, because each number will be larger than the last.

Don’t forget to set current_number to the most recent number that we saw.

This code should print nothing at all because each number is greater than the last.

Two more collection types left! The next one is more rarely used than the others so far but has a very clear purpose.

6.1.3 BinaryHeap

A BinaryHeap is an interesting collection type because it is mostly unordered but has a bit of order. It keeps the item with the greatest value in the front, but the other items are in any order. Some languages call this a priority queue. We will use another list of items for an example, but this time, smaller:

use std::collections::BinaryHeap;
 
fn main() {
    let many_numbers = vec![0, 5, 10, 15, 20, 25, 30];
    let mut heap = BinaryHeap::new();                                     
    for num in many_numbers {
        heap.push(num);
    }
    println!("First item is largest, others are out of order: {heap:?}");
    while let Some(num) = heap.pop() {                                    
        println!("Popped off {num}. Remaining numbers are: {heap:?}");
    }
}

Note that these numbers are in order. They won’t be in the same order once we put them inside our BinaryHeap, though.

The .pop() method returns Some(number) if a number is there, and None if not. It pops from the front, which is where the item with the greatest value is.

This prints

First item is largest, others are out of order: [30, 15, 25, 0, 10, 5, 20]
Popped off 30. Remaining numbers are: [25, 15, 20, 0, 10, 5]
Popped off 25. Remaining numbers are: [20, 15, 5, 0, 10]
Popped off 20. Remaining numbers are: [15, 10, 5, 0]
Popped off 15. Remaining numbers are: [10, 0, 5]
Popped off 10. Remaining numbers are: [5, 0]
Popped off 5. Remaining numbers are: [0]
Popped off 0. Remaining numbers are: []

You can see that the number in the 0th index is always largest: 30, 25, 20, 15, 10, 5, and then 0. But the other items are all in random order.

A good way to use a BinaryHeap is for a collection of things to do. Here we create a BinaryHeap<(u8, &str)> where the u8 is a number for the importance of the task. The &str is a description of what to do:

use std::collections::BinaryHeap;
 
fn main() {
    let mut jobs = BinaryHeap::new();
 
    jobs.push((100, "Reply to email from the CEO"));   
    jobs.push((80, "Finish the report today"));
    jobs.push((5, "Watch some YouTube"));
    jobs.push((70, "Tell your team members thanks for always working hard"));
    jobs.push((30, "Plan who to hire next for the team"));
 
    for (_, job) in jobs {                             
        println!("You need to: {job}");
    }
}

Adds jobs to do throughout the day

Here’s a nice example of destructuring again. We don’t care to print out the number, just the description.

Because the largest item always shows up first, this will always print

You need to: Reply to email from the CEO
You need to: Finish the report today
You need to: Tell your team members thanks for always working hard
You need to: Plan who to hire next for the team
You need to: Watch some YouTube

Finally, we have the famous VecDeque, a sort of special Vec that also has a very clear purpose.

6.1.4 VecDeque

A VecDeque (pronounced “vec-deck”) is a Vec that is optimized for (i.e., good at) popping items both off the front and the back. Rust has VecDeque because Vecs are great for popping off the back (the last item) but not so great off the front. When you use .pop() on a Vec, it just takes off the last item on the right, and nothing else is moved. But if you remove an item from anywhere else inside a Vec, all the items to the right of it are moved over one position to the left. You can see this in the description for .remove():

Removes and returns the element at position index within the vector,
shifting all elements after it to the left.

Take this example:

fn main() {
    let mut my_vec = vec![9, 8, 7, 6, 5];
    my_vec.remove(0);
}

What happens when we remove the number 9 from index 0? Well, all the other elements have to move one step left. The 8 in index 1 will move to index 0, the 7 in index 2 will move to index 1, and so on. It’s sort of like a traffic jam. If you remove one car from the front, then all the rest have to move forward a bit.

With a big Vec, this is a lot of work for the computer. In fact, if you run it on the Playground, it will probably just give up because it’s too much work. And if you run this on your own computer, it should take about a minute to finish:

fn main() {
    let mut my_vec = vec![0; 600_000];
    for _ in 0..600000 {
        my_vec.remove(0);
    }
}

It’s easy to imagine why. We start with a Vec of 600,000 zeros. Every time you use remove(0) on it, it moves each remaining zero one space to the left. And then it does it 600,000 times. So that’s 599,999 items moved, then 599,998 items moved, then 599,997 moves, and so on—600,000 times in total.

You don’t have to worry about that with a VecDeque (it uses something called a ring buffer to make this possible). In general, it is a bit slower than a Vec, but if you have to do things on both ends, it is much faster, thanks to the buffer. You can use VecDeque::from() with a Vec to make one. Our previous code then looks like this:

use std::collections::VecDeque;
 
fn main() {
    let mut my_vec = VecDeque::from(vec![0; 600000]);
    for i in 0..600000 {
        my_vec.pop_front();   
    }
}

pop_front is like .pop but for the front.

It is now much faster, and the code on the Playground should finish in under a second.

That’s the last collection type we have to learn in this book. For the rest of the chapter, we’re going to change subjects a bit and learn some tips about error handling.

6.2 The ? operator

There is an even shorter way to deal with Result, shorter than match and even shorter than if let. It is called the “question mark operator,” and you simply type ? to use it. After anything that returns a Result, you can add ?. This will

In other words, it does almost everything for you.

NOTE The ? operator works with Option, too, although the majority of the time you see it used to handle a Result.

We can try this with .parse() again. We will write a function called parse_and_log_ str that tries to turn a &str into an i32, prints a message, and returns the number. It looks like this:

use std::num::ParseIntError;                                       
 
fn parse_and_log_str(input: &str) -> Result<i32, ParseIntError> {
    let parsed_number = input.parse::<i32>()?;                     
    println!("Number parsed successfully into {parsed_number}");
    Ok(parsed_number)
}

How did we know where to find this error type? We’ll find out in just a moment.

This is the key line in the function. If the &str parses successfully, you will have a variable called parsed_number that is an i32. If it doesn’t parse successfully, the function ends here and returns an error.

This function takes a &str. If it is Ok, it gives an i32 wrapped in Ok. If it is an Err, it returns a ParseIntError, and the function is over. So when we try to parse the number, we add ?, which means “check whether it is an error, and give what is inside the Result if it is Ok.” If it is not Ok, it will return the error, and the function ends. But if it is Ok, it will go to the next line, and the function will not need to return early. This is why we can then type println!("Number parsed successfully into {parsed_ number}"); because if it had returned an Err, the function would have already returned, and we never would have reached this line.

On the last line is the number inside of Ok(). We need to wrap it in Ok because the return value is Result<i32, ParseIntError>, not i32.

By the way, the ? operator is just short for a match. You could write the parse_str() function without ? , but it is a lot more typing. Here is what ? does:

use std::num::ParseIntError;
 
fn parse_and_log_str(input: &str) -> Result<i32, ParseIntError> {
    let parsed_number = match input.parse::<i32>() {
        Ok(number) => number,
        Err(e) => return Err(e),
    };
    println!("Number parsed successfully into {parsed_number}");
    Ok(parsed_number)
}

Now, we can try out our function. Let’s see what it does with a Vec of &strs.

use std::num::ParseIntError;
 
fn parse_and_log_str(input: &str) -> Result<i32, ParseIntError> {
    let parsed_number = input.parse::<i32>()?;
    println!("Number parsed successfully into {parsed_number}");
    Ok(parsed_number)
}
 
fn main() {
    let str_vec = vec!["Seven", "8", "9.0", "nice", "6060"];
    for item in str_vec {
        let parsed = parse_and_log_str(item);
        println!("Result: {parsed:?}");
    }
}

This prints

Result: Err(ParseIntError { kind: InvalidDigit })
Number parsed successfully into 8
Result: Ok(8)
Result: Err(ParseIntError { kind: InvalidDigit })
Result: Err(ParseIntError { kind: InvalidDigit })
Number parsed successfully into 6060
Result: Ok(6060)

You might be wondering how we know to use std::num::ParseIntError. One easy way is to “ask” the compiler again (although if you have Rust installed and an IDE like Visual Studio, then hovering your mouse over the type will show what the signature is):

fn main() {
    let failure = "Not a number".parse::<i32>();
    failure.rbrbrb();        
}

Compiler: “What is rbrbrb()???”

The compiler doesn’t understand why we are trying to call a method called .rbrbrb() on a Result enum, and tells us what type we are trying to use this method on:

error[E0599]: no method named `rbrbrb` found for enum 
`std::result::Result<i32, std::num::ParseIntError>` in the current scope
 --> src\main.rs:3:13
  |
3 |     failure.rbrbrb();
  |             ^^^^^^ method not found in `std::result::Result<i32,
  std::num::ParseIntError>`

So std::result::Result<i32, std::num::ParseIntError> is the signature we need.

We don’t need to write std::result::Result because Result is always in scope (in scope = ready to use). Rust does this for all the types we use a lot, so we don’t have to write std::result::Result, std::collections::Vec, etc. This full path is known as the fully qualified path.

In our example with our parse_int() function, we are handling the result of the function inside main(). But is it possible to use the question mark inside main()? After all, main is expecting a return type of (), but the question mark operator here returns a Result, not (). The answer is yes: main can return a few other things besides (), one of which is Result (see https://doc.rust-lang.org/std/process/trait.Termination.html). Let’s try parsing some numbers in main() and see what happens:

use std::num::ParseIntError;
 
fn main() -> Result<(), ParseIntError> {
    for item in vec!["89", "8", "9.0", "eleven", "6060"] {
        let parsed = item.parse::<u32>()?;    
        println!("{parsed}");
    }
    Ok(())                                    
}

Here we use the question mark operator. What do you think will happen when we get a number that fails to parse?

Now main() expects a Result. If all of the numbers parse, we will reach this line and now simply wrap an () inside of Ok.

Here is the output:

89
8
Error: ParseIntError { kind: InvalidDigit }

As you can see, the third item failed to parse and main() returned early instead of trying to parse the rest. Note that this was not a panic: the main() function simply returned early with an Err value.

So using the question mark operator in main should be used when you don’t mind ending the whole program early when there is an error. One good example of this is if you are starting up an app that has a lot of components that all need to work properly: a certain file needs to be found, a connection to the database needs to be set up, and so on. In that case, you definitely want the program to end early if any of these go wrong so you can find the problem and fix it.

The ? operator becomes even more useful once we know how to deal with multiple error types because you can use one ? after another in a single line. At this point in the book, we don’t know how to work with multiple error types at the same time, but we can put together a useless but quick example that will at least give you a taste. Instead of making an i32 with .parse(), we’ll do a lot more. We’ll make a u16, then turn it to a String, then a u32, then to a String again, and finally to an i32:

use std::num::ParseIntError;
 
fn parse_str(input: &str) -> Result<i32, ParseIntError> {
    let parsed_number = input
        .parse::<u16>()?
        .to_string()
        .parse::<u32>()?
        .to_string()
        .parse::<i32>()?;
    println!("Number parsed successfully into {parsed_number}");
    Ok(parsed_number)
}
 
fn main() {
    let str_vec = vec!["Seven", "8", "9.0", "nice", "6060"];
    for item in str_vec {
        let parsed = parse_str(item);
        println!("{parsed:?}");
    }
}

The output is the same as the example before:

Err(ParseIntError { kind: InvalidDigit })
Number parsed successfully into 8
Ok(8)
Err(ParseIntError { kind: InvalidDigit })
Err(ParseIntError { kind: InvalidDigit })
Number parsed successfully into 6060
Ok(6060)

At the moment, we only know how to use ? when returning a single error type. Here is why. Imagine that you want to take some bytes, turn them into a String, and then parse it into a number. First, you need to successfully create a String from the bytes using a method called String::from_utf8(). And then it needs to successfully parse into a number. We could write it like this:

fn turn_into_string_and_parse(bytes: Vec<u8>) -> i32 {
    let as_string = String::from_utf8(bytes).unwrap();
    let as_num = as_string.parse::<i32>().unwrap();
    as_num
}
 
fn main() {
    let num = turn_into_string_and_parse(vec![49, 53, 53]);
    println!("{num}");
}

Fortunately, we give it an input that worked: the bytes 49, 53, and 53 turn into the String "155", which parses successfully into a 155. But this is bad error handling (actually, it’s no error handling). If any input returns an Err, the whole program will panic. It would be nice to use the ? operator here in both cases. But as we start writing the code, we get to this point and stop:

use std::num::ParseIntError;
use std::string::FromUtf8Error;
 
fn turn_into_string_and_parse(bytes: Vec<u8>) -> 
Result<i32, ????> {                                       
    let num = String::from_utf8(bytes)?.parse::<i32>()?;    
    Ok(num)
}

What will the error type be? Two possible errors can be returned, but we only know how to return one.

This is what we would like to write if we only knew how. Then we could handle the error and do everything we want on a single line.

The problem is the return type. If String::from_utf8() fails, it will return Err<FromUtf8Error>. And if .parse() fails, it will return an Err<ParseIntError>. But we can’t return a Result<i32, ParseIntError or FromUtf8Error>—the errors are completely different types. To solve this requires learning a lot more about traits. We will start to learn about traits in the next chapter, and by chapter 13, we will finally know enough to solve this problem. In the meantime, let’s think about panic! and .unwrap() some more.

6.3 When panic and unwrap are good

Rust has a panic! macro that you can use to make it panic. It is easy to use:

fn main() {
    panic!();
}

Easy! The program panics and gives us this output:

thread 'main' panicked at 'explicit panic', src/main.rs:2:5

Or you can panic with a message:

fn main() {
    panic!("Time to panic!");
}

This time, the message Time to panic! displays when you run the program:

thread 'main' panicked at 'Time to panic!', src/main.rs:2:5

You will remember that src/main.rs is the directory and filename, and 2:3 is the line and column name. With this information, you can find the code and fix it.

panic! is a good macro to use to make sure that you know when something changes in your code. For example, the function called print_all_three_things always prints index [0], [1], and [2] from a vector. It is okay at the moment because we always give it a vector with three items:

fn print_all_three_things(vector: Vec<i32>) {
    println!("{}, {}, {}", vector[0], vector[1], vector[2]);
}
 
fn main() {
    let my_vec = vec![8, 9, 10];
    print_all_three_things(my_vec);
}

It prints 8, 9, 10, and everything is fine.

But imagine that later on we write more and more code and forget that my_vec can only be three things. Now my_vec in this part has six things:

fn main() {
  let my_vec = vec![8, 9, 10, 10, 55, 99];    
  print_all_three_things(my_vec);
}
 
fn print_all_three_things(vector: Vec<i32>) {
  println!("{}, {}, {}", vector[0], vector[1], vector[2]);
}

Now my_vec has six things.

No error happens because [0], [1], and [2] are all inside this longer vector. But what if it was really important to only have three items in the Vec? We wouldn’t know that there was a problem because the program doesn’t panic. This is known as a logic bug: the code runs fine, but the logic is wrong. Telling the code to panic in certain cases is a good way to watch out for logic bugs:

fn print_all_three_things(vector: Vec<i32>) {
    if vector.len() != 3 {
        panic!("my_vec must always have three items");
    }
    println!("{}, {}, {}", vector[0], vector[1], vector[2]);
}
 
fn main() {
    let my_vec = vec![8, 9, 10, 10, 55, 99];
    print_all_three_things(my_vec);
}

And now this code will panic as we told it to:

thread 'main' panicked at 'my_vec must always have three items',
src/main.rs:3:9

Thanks to panic!, we now remember that my_vec should only have three items. So panic! is a good macro to create reminders in your code.

There are three other macros that are similar to panic! that you use a lot in testing. They are assert!, assert_eq!, and assert_ne!. Here is what they mean:

Some examples are as follows:

fn main() {
    let my_name = "Loki Laufeyson";
 
    assert!(my_name == "Loki Laufeyson");
    assert_eq!(my_name, "Loki Laufeyson");
    assert_ne!(my_name, "Mithridates");
}

This will do nothing because all three assert macros are okay (this is what we want).

You can also add a message to these methods if you want:

fn main() {
    let my_name = "Loki Laufeyson";
 
    assert!(
        my_name == "Loki Laufeyson",
        "Name {my_name} is wrong: should be Loki Laufeyson"
    );
    assert_eq!(
        my_name, "Loki Laufeyson",
        "{my_name} and Loki Laufeyson should be equal"
    );
    assert_ne!(
        my_name, "Mithridates",
        "You entered {my_name}. Input must not equal Mithridates"
    );
}

These messages will only display if the program panics. So, if you run

fn main() {
    let my_name = "Mithridates";
 
    assert_ne!(
        my_name, "Mithridates",
        "You entered {my_name}. Input must not equal Mithridates"
    );
}

it will display

thread 'main' panicked at 'assertion failed: `(left != right)`
  left: `"Mithridates"`,
  right: `"Mithridates"`: You entered Mithridates. Input must not equal
  Mithridates', src\main.rs:4:5

The output is telling us, “You said that left != right, but left == right.” And it displays our custom message that says You entered Mithridates. Input must not equal Mithridates.

Unwrapping is also good when you are first writing your program and you want it to crash when there is a problem. Later, when your code is finished, it is good to change unwrap() to something else that won’t crash. (You don’t want a program to panic while a customer is using it.)

You can also use expect, which is like unwrap but a bit better because you give it your own message. Textbooks usually give this advice: “If you use unwrap a lot, at least use expect for better error messages.”

This will crash:

fn get_fourth(input: &Vec<i32>) -> i32 {
    let fourth = input.get(3).unwrap();
    *fourth
}
 
fn main() {
    let my_vec = vec![9, 0, 10];
    let fourth = get_fourth(&my_vec);
}

The error message is

thread 'main' panicked at 'called Option::unwrap() on a None
value', src\main.rs:7:18.

Now we write our own message with expect:

fn get_fourth(input: &Vec<i32>) -> i32 {
    let fourth = input.get(3).expect("Input vector needs at least 4 items");
    *fourth
}
 
fn main() {
    let my_vec = vec![9, 0, 10];
    let fourth = get_fourth(&my_vec);
}

It crashes again, but the error is better:

thread 'main' panicked at 'Input vector needs at least 4 items', src\main.rs:7:18 

So expect is a little better than unwrap, but it will still panic on None. The .expect() method is also good for documentation because it allows anyone reading your code to have an idea of what could go wrong and where.

Now, here is an example of a bad practice: a function that tries to unwrap two times. It takes a Vec<Option<i32>>, so maybe each part will have a Some<i32> or maybe a None:

fn try_two_unwraps(input: Vec<Option<i32>>) {
    println!("Index 0 is: {}", input[0].unwrap());
    println!("Index 1 is: {}", input[1].unwrap());
}
 
fn main() {
    let vector = vec![None, Some(1000)];   
    try_two_unwraps(vector);
}

This vector has a None, so it will panic.

The message is

thread 'main' panicked at 'called Option::unwrap() on a None
value', src\main.rs:2:32

We’re not sure if it was the first unwrap or the second unwrap until we check the line, and in a large codebase, it can take a bit of time to find the exact file and line where a panic occurred. It would be better to check the length and also not to unwrap. But with expect, at least it will be a little better. Here it is with expect:

fn try_two_unwraps(input: Vec<Option<i32>>) {
    println!(
        "Index 0 is: {}",
        input[0].expect("The first unwrap had a None!")
    );
    println!(
        "Index 1 is: {}",
        input[1].expect("The second unwrap had a None!")
    );
}
 
fn main() {
    let vector = vec![None, Some(1000)];
    try_two_unwraps(vector);
}

So that is a bit better:

thread 'main' panicked at 'The first unwrap had a None!', src\main.rs:2:32 

We have the line number as well so we can find it.

There is another method called .unwrap_or() that is useful if you want to always have a value that you want to choose. If you do this, it will never panic, which is good because your program won’t panic, but maybe not good if you want the program to panic if there’s a problem.

But usually, we don’t want our program to panic, so .unwrap_or() is a good method to use:

fn main() {
    let my_vec = vec![8, 9, 10];
 
    let fourth = my_vec.get(3).unwrap_or(&0);    
    println!("{fourth}");
}

If .get doesn’t work, we will make the value &0. .get() returns a reference, so we need &0 and not 0 to match it. You can also write "let *fourth" with a * if you want fourth to be a 0 and not a &0.

This prints 0 because .unwrap_or(&0) gives a zero even if it is a None. It will never panic.

This chapter was a lot of expansion of what you already know, so it probably wasn’t too hard. You learned some extra collection types on top of the ones you already know, and we learned more about error handling. The question mark operator is new, but it’s still based on what you already know: matching on a Result. But in the next chapter, we will learn something new: how traits work and how to write our own traits.

Summary