Option
—when an operation might produce a value but might notResult
—when an operation might succeed but might notRust is a strict language with concrete types, but after this chapter, you’ll have three important tools to work with. Generics let you describe to Rust “some sort of type” that Rust will turn into a concrete type without you having to do it. After that, we’ll learn about two interesting enums called Option
and Result
. Option
tells Rust what to do when there might not be a value, and Result
tells Rust what to do when something might go wrong.
We’ve known since chapter 1 that Rust needs to know the concrete type for the input and output of a function. The following return_item()
function has i32
for both its input and output, and no other type will work—only i32
:
fn return_item(item: i32) -> i32 { println!("Here is your item."); item } fn main() { let item = return_item(5); }
But what if you want the function to accept more than i32
? It would be annoying if you had to write all these functions:
fn return_i32(number: i32) -> i32 { }
fn return_i16(number: i16) -> i16 { }
fn return_u8(number: u8) -> u8 { } ①
You can use generics for this. Generics basically means “maybe one type, maybe another type.”
For generics, you use angle brackets with the type inside, like this: <T>
. This means “any type you put into the function.” Rust programmers usually use one capital letter for generics (T, U, V
, etc.), but the name doesn’t matter, and you don’t have to use one letter. The only part that matters is the angle brackets: <>
.
This is how you change the function to make it generic:
fn return_item<T>(item: T) -> T { println!("Here is your item."); item } fn main() { let item = return_item(5); }
The important part is the <T>
after the function name. Without this, Rust will think that T
is a concrete (concrete = not generic) type, like String
or i8
. When talking about generics, people say that something is “generic over (name of the type).” So, for the return_item
function, you would say, “The function return_item
is generic over type T
.”
Type names in generics are easier to understand if we choose a name instead of just T
. See what happens when we change T
to MyType
:
fn return_item(item: MyType) -> MyType { println!("Here is your item."); item }
The compiler gives the error cannot find type `MyType` in this scope
. As you can see, MyType
is concrete, not generic: the compiler is looking for something called MyType
and can’t find it. To tell the compiler that MyType
is generic, we need to write it inside the angle brackets:
fn return_item<MyType>(item: MyType) -> MyType { println!("Here is your item."); item } fn main() { let item = return_item(5); }
Because of the angle brackets, now the compiler sees that this is a generic type that we are calling MyType
. Without the angle brackets, it’s not generic.
Let’s look at the first part of the signature one more time to make sure we understand it. Here is the signature:
fn return_item<MyType>(item: MyType)
<MyType>
—Ah, the function is generic! There will be some type in it that the programmer wants to call MyType
.
item: MyType
—The function takes a variable called item
, which will be of the type MyType
that the programmer declared inside the angle brackets.
You could call it anything as long as you put it in angle brackets so the compiler knows that the type is generic. Now, we will go back to calling the type T
because Rust code usually uses single letters. You can choose your own names in your own generic code, but it’s good to get used to seeing these single letters and recognizing them as a hint that we are dealing with generic types.
You will remember that some types in Rust are Copy
, some are Clone
, some are Display
, some are Debug
, and so on. In other words, they implement the traits Copy, Clone
, and so on. With Debug
, we can print with {:?}
.
The following code sample tries to print a generic item called T
, but it won’t work. Can you guess why?
fn print_item<T>(item: T) { println!("Here is your item: {item:?}"); } fn main() { print_item(5); }
The function print_item()
needs T
to have Debug
to print item
, but is T
a type with Debug
? Maybe not. Maybe it doesn’t have #[derive(Debug)]
—who knows? The compiler doesn’t know either, so it gives an error:
error[E0277]: `T` doesn't implement `Debug`
--> src/main.rs:2:34
|
2 | println!("Here is your item: {item:?}");
| ^^^^^^^^ `T` cannot be formatted
➥using `{:?}` because it doesn't implement `Debug`
There’s no guarantee that T
implements Debug
. Somebody using the function might pass in a type that implements Debug
, but also might not! Do we implement Debug
for T
? No, because we don’t know what T
is—right now, anyone can use the function and put in any type. Some of them will have Debug
; some won’t.
However, we can tell the function: “Don’t worry, any type T
that we pass into this function will implement Debug.
” It’s sort of a promise to the compiler:
use std::fmt::Debug; ① fn print_item<T: Debug>(item: T) { ② println!("Here is your item: {item:?}"); } fn main() { print_item(5); }
① The Debug trait is located at std::fmt::Debug.
② <T: Debug> is the important part.
Now the compiler knows: “Okay, this type T
is going to have Debug.
” Now the code works because i32
has Debug
. Now, we can give it many types: String, &str
, and so on because they all have Debug
. The code will now compile, and the compiler won’t let any type be the variable item
in this function unless it has Debug
(you can’t trick the compiler).
Now, we can create a struct and give it Debug
with #[derive(Debug)]
, so we can print it, too. Our function can take i32
, the struct Animal
, and more:
use std::fmt::Debug; #[derive(Debug)] struct Animal { name: String, age: u8, } fn print_item<T: Debug>(item: T) { println!("Here is your item: {item:?}"); } fn main() { let charlie = Animal { name: "Charlie".to_string(), age: 1, }; let number = 55; print_item(charlie); print_item(number); }
Here is your item: Animal { name: "Charlie", age: 1 } Here is your item: 55
Sometimes, we need more than one generic type in a generic function. To do this, we have to write out each generic type name and think about how we want to use it. What traits should each type be able to use?
In the following example, we want two types. First, we want a type called T
that we would like to print. Printing with {}
is nicer, so we will require Display
for T
.
Next is a generic type that we will call U
and two variables, num_1
and num_2
, which will be of type U
. We want to compare them, so it will need PartialOrd
. The PartialOrd
trait lets us use comparison operators like <, >, ==
, and so on. But we want to print them, too, so we require Display
for U
as well. You can use +
if you want to indicate more than one trait.
To sum up, <U: Display + PartialOrd>
means there is a generic type that we are calling U
, and it needs to have these two traits:
use std::fmt::Display; use std::cmp::PartialOrd; fn compare_and_display<T: Display, U: Display + PartialOrd>(statement: T, ➥input_1: U, input_2: U) { println!("{statement}! Is {input_1} greater than {input_2}? {}", ➥input_1 > input_2); } fn main() { compare_and_display("Listen up!", 9, 8); }
This prints Listen up!! Is 9 greater than 8? true
. So,
fn compare_and_display<T: Display, U: Display + PartialOrd>(statement: T,
➥num_1: U, num_2: U)
The first type is T
, and it is generic. It must be a type that can print with {}
.
The next type is U
, and it is generic. It must be a type that can print with {}
. Also, it must be a type that can be compared (so that it can use >, <
, and ==
).
We can give compare_and_display()
different types if we want. The variable statement
can be a String
, a &str
, or anything with Display
.
To make generic functions easier to read, we can also use the keyword where
right before the code block:
use std::cmp::PartialOrd; use std::fmt::Display; fn compare_and_display<T, U>(statement: T, num_1: U, num_2: U) ① where ② T: Display, U: Display + PartialOrd, { println!("{statement}! Is {num_1} greater than {num_2}? {}", num_1 > num_2); } fn main() { compare_and_display("Listen up!", 9, 8); }
① Now the part after compare_and_display only has <T, U>, which is a lot cleaner to read.
② Then we use the where keyword and indicate the traits needed on the following lines.
Using where
is a good idea when you have many generic types. Also note the following:
If you have one variable of type T
and another variable of type T
, they must be the same type.
If you have one variable of type T
and another variable of type U
, they can be different types. But they can also be the same. For example,
use std::fmt::Display; fn say_two<T: Display, U: Display>(statement_1: T, ➥statement_2: U) { ① println!("I have two things to say: {statement_1} and {statement_2}"); } fn main() { say_two("Hello there!", String::from("I hate sand.")); ② say_two(String::from("Where is Padme?"), String::from("Is she all right?")); ③ }
① Types T and U both need to implement Display, but they can be different types.
② Type T is a &str, but type U is a String. No problem: both of these implement Display.
③ Here both types are String. No problem: T and U don’t have to be different types.
I have two things to say: Hello there! and I hate sand. I have two things to say: Where is Padme? and Is she all right?
Now that we understand both enums and generics, we can understand Option
and Result
. These are two enums that Rust uses to help us write code that will not crash.
The beginning of the chapter describes Option
as a type “for when you might get a value, but maybe not,” and Result
as a type “for when an operation might succeed, but maybe not.” If you remember that, you should have a good idea of when to use one and when to use the other.
A person in real life, for example, would have an Option<Spouse>
. You might have one, and you might not. Not having a spouse simply means that you don’t have a spouse, but it’s not an error—just something that might or might not exist.
But the function go_to_work()
would return a Result
because it might fail! Most times go_to_work()
succeeds, but one day, it might snow too much, and you have to stay home.
Meanwhile, simple functions like print_string()
or add_i32()
always produce output and can’t fail, so they don’t need to deal with Option
or a Result
. With that in mind, let’s start with Option
.
You use Option
when something might or might not exist. When a value exists, it is Some(value),
and when it doesn’t, it’s None
. Here is an example of bad code that can be improved with Option
:
fn take_fifth_item(value: Vec<i32>) -> i32 { value[4] } fn main() { let new_vec = vec![1, 2]; let index = take_fifth_item(new_vec); }
This code panics when we run it. Here is the message:
thread 'main' panicked at 'index out of bounds:
➥the len is 2 but the index is 4', src\main.rs:34:5
Panic means that the program stops before the problem happens. Rust sees that the function wants something impossible and stops. It “unwinds the stack” (takes the values off the stack) and tells you, “Sorry, I can’t do that.”
To fix this, we will change the return type from i32
to Option<i32>
. This means “give me a Some(i32)
if it’s there, and give me None
if it’s not.” We say that the i32
is “wrapped” in an Option
, which means it’s inside an Option
. If it’s Some
, you have to do something to get the value out:
fn try_take_fifth(value: Vec<i32>) -> Option<i32> {
if value.len() < 5 { ①
None
} else {
Some(value[4])
}
}
fn main() {
let small = vec![1, 2];
let big = vec![1, 2, 3, 4, 5];
println!("{:?}, {:?}", try_take_fifth(small), try_take_fifth(big));
}
① .len() gives the length of the Vec. Here, we are checking that the length is at least 5.
This prints None, Some(5)
. Our program doesn’t panic anymore, so this is better than before. But in the second case, the value 5
is still inside the Option
. How do we get the 5
out of there?
We can get the value inside an Option
with a method called .unwrap()
, but be careful with .unwrap()
. It’s just like unwrapping a present: maybe there’s something good inside, or maybe there’s an angry snake inside. You only want to .unwrap()
if you are sure. If you unwrap a value that is None
, the program will panic:
fn try_take_fifth(value: Vec<i32>) -> Option<i32> {
if value.len() < 5 {
None
} else {
Some(value[4])
}
}
fn main() {
let small = vec![1, 2];
let big = vec![1, 2, 3, 4, 5];
println!("{:?}, {:?}",
try_take_fifth(small).unwrap(), ①
try_take_fifth(big).unwrap()
);
}
① This one returns None. .unwrap() will panic!
thread 'main' panicked at 'called
➥`Option::unwrap()` on a `None` value', src\main.rs:14:9
But we don’t have to use .unwrap()
. We can use a match
instead. With match
, we can print the value if we have Some
and not touch it if we have None
. For example:
fn try_take_fifth(value: Vec<i32>) -> Option<i32> { if value.len() < 5 { None } else { Some(value[4]) } } fn handle_options(my_option: &Vec<Option<i32>>) { for item in my_option { match item { Some(number) => println!("Found a {number}!"), None => println!("Found a None!"), } } } fn main() { let small = vec![1, 2]; let big = vec![1, 2, 3, 4, 5]; let mut option_vec = Vec::new(); ① option_vec.push(try_take_fifth(small)); ② option_vec.push(try_take_fifth(big)); ③ handle_options(&option_vec); ④ }
① Makes a new Vec to hold our Options. The vec is type: Vec<Option<i32>>. That means a Vec of Option<i32>.
② This pushes None into the Vec.
③ This pushes Some(5) into the vec.
④ handle_option() looks at every option in the Vec. It prints the value if it is Some. It doesn’t touch it if it is None.
Found a None! Found a 5!
This was a good example of pattern matching. Some(number)
is a pattern, and None
is another pattern. We use match
to decide what to do when each of these patterns happens. The Option
type has two possible patterns, so we have to decide what to do when we see one pattern and what to do when we see another.
So, what does the actual Option
type look like? Because we know generics, we are able to read the code for Option
. It is quite simple—just an enum:
enum Option<T> { None, Some(T), }
The important point to remember is with Some
, you have a value of type T
(any type). Also, note that the angle brackets after the enum
name around T
tell the compiler that it’s generic. It has no trait like Display
or anything to limit it; it can be anything. But with None
, you don’t have any value inside.
So, in a match
statement for Option
you can’t say
Some(value) => println!("The value is {}", value), None(value) => println!("The value is {}", value),
because None
doesn’t hold a T
inside it. Only the Some
variant will hold a value.
There are easier ways to use Option
. In the next code sample, we will use a method called .is_some()
to tell us if it is Some
. (Yes, there is also a method called .is_none()
.) Using this means that we don’t need handle_option()
anymore:
fn try_take_fifth(value: Vec<i32>) -> Option<i32> { if value.len() < 5 { None } else { Some(value[4]) } } fn main() { let small = vec![1, 2]; let big = vec![1, 2, 3, 4, 5]; for vec in vec![small, big] { let inside_number = try_take_fifth(vec); if inside_number.is_some() { ① println!("We got: {}", inside_number.unwrap()); ② } else { println!("We got nothing."); } } }
① The .is_some() method returns true if we get Some, false if we get None.
② We already checked that inside_number is Some, so it is safe to use .unwrap(). There is an easier way to do this called 'if let' that we will learn soon.
We got nothing. We got: 5
Now imagine that we wanted this take_fifth()
function or some other function to give us a reason for why it fails. We don’t want to get None
; we want to know why it failed. When it fails, we’d like to have some information on what went wrong so we can do something about it. Something like Error: Vec wasn't long enough to get the fifth item
. That’s what Result
is for! Let’s learn that now.
Result
looks similar to Option
, but here is the difference:
You often see both Option
and Result
at the same time. For example, you might want to get data from a server. First, you use a function to connect. The connection might fail, so that’s a Result
. And after connecting, there might not be any data. That’s an Option
. So the entire operation would be an Option
inside a Result
: a Result<Option<SomeType>>
.
To compare the two, here are the signatures for Option
and Result
:
enum Option<T> { None, Some(T), } enum Result<T, E> { Ok(T), Err(E), }
Note that Result
has a value inside of Ok
and inside of Err
. That is because errors are supposed to contain information that describes what went wrong. Also, note that Ok
holds a generic type T
, and Err
holds a generic type E
. As we learned in this chapter, they can be different types (and usually are) but could be the same.
Result<T, E>
means you need to think of what you want to return for Ok
and what you want to return for Err
. In fact, you can return anything you like. Even returning a ()
in each case is okay:
fn check_error() -> Result<(), ()> { Ok(()) } fn main() { check_error(); }
check_error()
says, “Return ()
if we get Ok
, and return ()
if we get Err
.” Then we return Ok
with a ()
inside it. The program works with no problem!
The compiler gives us an interesting warning, though:
warning: unused `std::result::Result` that must be used --> src\main.rs:6:5 | 6 | check_error(); | ^^^^^^^^^^^^^^ | = note: `#[warn(unused_must_use)]` on by default = note: this `Result` may be an `Err` variant, which should be handled
This is true: we only returned the Result
, but it could have been an Err
. So, let’s handle the error a bit, even though we’re still not really doing anything:
fn see_if_number_is_even(input: i32) -> Result<(), ()> { if input % 2 == 0 { return Ok(()) } else { return Err(()) } } fn main() { if see_if_number_is_even(5).is_ok() { println!("It's okay, guys") } else { println!("It's an error, guys") } }
This prints It's an error, guys
. We just handled our first error! Something went wrong, we told Rust what to do in case of an error, and the program didn’t panic. That’s what Result
helps you with.
The four methods to easily check the state of an Option
or a Result
are as follows:
Sometimes a function with Result
will use a String
for the Err
value. This is not a true error type yet, but it contains some information and is a little better than what we’ve done so far. Here’s a simple example showing a function that expects the number 5 and gives an error otherwise. Using a String
now lets us show some extra information:
fn check_if_five(number: i32) -> Result<i32, String> { match number { 5 => Ok(number), _ => Err(format!("Sorry, bad number. Expected: 5 Got: {number}")), } } fn main() { for number in 4..=7 { println!("{:?}", check_if_five(number)); } }
Err("Sorry, bad number. Expected: 5 Got: 4") Ok(5) Err("Sorry, bad number. Expected: 5 Got: 6") Err("Sorry, bad number. Expected: 5 Got: 7")
Just like unwrapping a None
for Option
, using .unwrap()
on Err
will panic:
fn main() { let error_value: Result<i32, &str> = ➥Err("There was an error"); ① error_value.unwrap(); ② }
① A Result is just a regular enum, so we can create one whenever we like. Both Option and Result and their variants are already in scope, so we can just write Err instead of Result::Err.
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value:
➥"There was an error"', src/main.rs:3:17
This information helps you fix your code. src\main.rs:3:17
means “go to the folder src
, then the file main.rs
, and then to line 3
and column 17
where the error happened.” So you can go there to look at your code and fix the problem.
You can also create your own error types. Result functions in the standard library and other people’s code usually do this. For example, look at this function from the standard library:
pub fn from_utf8(vec: Vec<u8>) -> Result<String, FromUtf8Error>
This function takes a vector of bytes (u8
) and tries to make a String
. So the success case for the Result
is a String
, and the error case is FromUtf8Error
. You can give your error type any name you want. To make a type into a true error type in Rust, it needs to implement a trait called Error
. Doing so lets it be used in generic code that expects a type that implements Error
in the same way that generic code might expect a type to implement Debug, Display,
or PartialOrd
as we saw in this chapter.
We will start to learn about traits in detail in chapter 7, but we have some more things to learn before then. One of them is more pattern matching, as Rust has a lot of other ways to do pattern matching besides the match
keyword. Let’s see why we might want to use them instead of always using match
.
Using a match
with Option
and Result
sometimes requires a lot of code. For example, take the .get()
method, which is used on a Vec
to see whether there is a value at a given index. It returns an Option
:
fn main() { let my_vec = vec![2, 3, 4]; let get_one = my_vec.get(0); ① let get_two = my_vec.get(10); ② println!("{:?}", get_one); println!("{:?}", get_two); }
Some(2) None
We learned that matching is a safe way to work with an Option
. Let’s do that with a range from indexes 0 to 10 to see whether there are any values:
fn main() { let my_vec = vec![2, 3, 4]; for index in 0..10 { match my_vec.get(index) { Some(number) => println!("The number is: {number}"), None => {} } } }
The code works fine and prints what we expected:
The number is: 2 The number is: 3 The number is: 4
We weren’t doing anything in case of None
because we were only interested in what happens when we get a Some
, but we still had to tell Rust what to do in case of None
. Here we can make the code shorter by using if let
. Using if let
means “do something if it matches, and don’t do anything if it doesn’t.” if let
is for when you don’t care about matching for everything:
fn main() { let my_vec = vec![2, 3, 4]; for index in 0..10 { if let Some(number) = my_vec.get(index) { println!("The number is: {number}"); } } }
Two important points to remember:
if let Some(number) = my_vec.get(index)
means “if you get the pattern Some(number)
from my_vec.get(index)
.”
It uses one =
and not ==
because it is a pattern match, not a boolean.
Rust 1.65, released in November 2022, added an interesting new syntax called let else
. Let’s take a look at the same if let
example but add a let else
and see what makes it different. First, try reading this sample on your own and think about what is different between if let
and let else
:
fn main() { let my_vec = vec![2, 3, 4]; for index in 0..10 { if let Some(number) = my_vec.get(index) { ① println!("The number is: {number}"); } let Some(number) = my_vec.get(index) else { ② continue; }; println!("The number is: {number}"); } }
① This is the same if let from the previous example. It only cares about the Some pattern.
② This is the let else syntax. It also is only interested in the Some pattern and doesn’t care about None.
The difference between the two is as follows:
if let
checks to see whether my_vec.get()
gives the pattern Some
. If it gets a Some
, it calls the variable inside it number
and opens up a new scope inside the {}
curly brackets. Inside this scope, you are guaranteed to have a variable called number
. If .get()
doesn’t give the pattern Some
, it will simply do nothing and go to the next line.
let else
tries to make a variable number
from the pattern Some
. If you take out the else
part for a moment, you can see that it is trying to do this: let Some(number) = my_vec.get();
. In other words, it is trying to make this variable called number
.
But on the next line, it prints out the variable number
, so the variable has to exist at this point. So how can this work? It can work thanks to what is called diverging code. Diverging code is basically any code that lets you escape before going to the next line. The keyword continue
will do this, as will the keyword break
, an early return, and so on.
You can write as much as you want inside the block after else
, as long as you end with diverging code. For example,
fn main() { let my_vec = vec![2, 3, 4]; for index in 0..10 { let Some(number) = my_vec.get(index) else { ① println!("Looks like we got a None!"); println!("We can still do whatever we want inside this block"); println!("We just have to end with 'diverging code'"); print!("Because after this block, "); println!("the variable 'number' has to exist"); println!("Time to break the loop now, bye"); break; // return (); ② }; println!("The number is: {number}"); } }
① The block after else starts here. We have a whole block to do whatever we like. We end with break. This means the code will never get to the line below, which needs the variable called number.
② This is another example of diverging code. The keyword break; is used to break out of a loop, while return will return early from the function. The function main() returns an empty tuple, as we learned in chapter 3, so using return(); will return a (), the function will be over, and we never got to the line below.
You can see that we printed out quite a bit after we finally got a None
. And finally, at the end of all this printing, we use the keyword break
to diverge the code, and the program never got down to the next line. Here is the output:
The number is: 2 The number is: 3 The number is: 4 Looks like we got a None! We can still do whatever we want inside this block We just have to end with 'diverging code' Because after this block, the variable 'number' has to exist Time to break the loop now, bye
while let
is like a while
loop for if let
. Imagine that we have weather station data like this, in which we would like to parse certain strings into numbers:
["Berlin", "cloudy", "5", "-7", "78"] ["Athens", "sunny", "not humid", "20", "10", "50"]
To parse the numbers, we can use a method called parse::<i32>()
. First is .parse()
, which is the method name, followed by ::<i32>
, which is the type to parse into. It will therefore try to turn the &str
into an i32
and give it to us if it can. It returns a Result
because it might not work (for example, if you wanted it to parse the name “Billybrobby”—that’s not a number).
We will also use .pop()
. This takes the last item off of the vector:
fn main() { let weather_vec = vec![ vec!["Berlin", "cloudy", "5", "-7", "78"], vec!["Athens", "sunny", "not humid", "20", "10", "50"], ]; for mut city in weather_vec { println!("For the city of {}:", city[0]); ① while let Some(information) = city.pop() { ② if let Ok(number) = information.parse::<i32>() { ③ println!("The number is: {number}"); } ④ } } }
① In our data, every first item is the city name.
② while let Some(information) = city.pop() means to keep going until finally city runs out of items and .pop() returns None instead of Some.
③ Here we try to parse the variable we called information into an i32. This returns a Result. If it’s Ok(number), we will now have a variable called number that we can print.
④ Nothing happens here because we only care about getting an Ok. We never see anything that returns an Err.
For the city of Berlin: The number is: 78 The number is: -7 The number is: 5 For the city of Athens: The number is: 50 The number is: 10 The number is: 20
This chapter was the most “rusty” one so far. That’s because the three concepts you learned, generics, Option
, and Result
, aren’t even in most languages! So you’re already learning concepts that many other languages don’t even have.
But you probably also noticed that they aren’t weird, abstract concepts either. They are real, practical ways to help you write and work with your code. It’s nice not to have to write a new function for every type (generics). It’s nice to check whether a value is there or not (Option
). And it’s nice to check whether an error has happened and decide what to do if it does (Result
). The creators of Rust took some of these ideas from exotic languages but use them in a practical manner, as you saw in this chapter.
The next chapter isn’t too hard compared to this one. In it, you’ll learn some more about Result
and error handling, and we’ll see some more complex collection types than the ones you saw in chapter 3.
Generics let you use more than one type in your types or functions. Without them, you would need to repeat your code every time you wanted a different type.
You can write anything for generic types, but most of the time, people will just write T
.
After T
, you write what traits the type will have. Having more traits means T
can do more things. But it also means that the function can take fewer types because any type needs all of the traits you wrote.
Rust is still concrete, though: it turns generic functions into concrete ones at compile time. There’s nothing extra that happens at run time.
If you have a function that could panic, try turning its output into an Option
or a Result
. By doing so, you can write code that never crashes.
Don’t forget that the Err
value of a Result
doesn’t have to be an official error! If you are still learning Rust, returning a String
for the Err
value is easier.