Welcome to module 3
on handling Result and Option.
In this module, we'll discuss
two types provided by the Standard
Library:
the Result type
and the Option type.
We'll look at what to do when you get
a Result or Option returned
from a function or method call
to either use a successfully produced
value
or a recover
in the failure case.
We'll wrap up
with a discussion of how to turn recoverable
failures into unrecoverable panics
if we want.
Let's start with the Result
type.
Result is an enum defined by the
Standard Library.
It has two variants.
When a function or method returns
a value of type Result,
what it means is that, if everything went well,
you'll get back the Ok variant
of the result
with a value inside.
However, if something went wrong,
you'll get back the Err variant of the
result,
with a value inside that describes more about
what went wrong
and why.
As an example, let's use
the serde_json crate to parse
a string containing some JSON data
into a Rust data type.
Parsing JSON returns a result.
If the JSON is well-formed,
parsing will succeed
and we'll get an instance of a Rust type.
If we have invalid JSON that,
say, has a number as the key
or a trailing comma,
parsing will fail
and we'll get an error that describes the
problem.
Here's some code demonstrating success
and failure.
We're taking some raw strings containing
JSON, which you can imagine might
have come from an API call to a
web service,
and parsing it into instances of a
Person struct.
The first string is a well-formed
JSON
and the second string has a trailing comma.
When we run this code,
we see that the value in first
is the Ok variant containing an instance
of the Person struct.
The value in second is the Err
variant containing an instance
of a serde_json Error struct
that has information on why parsing
failed.
Next, let's look at the Option
type.
Option is similar to Result in
that it also has two variants.
Rather than representing success
and failure that Result does,
the variants of Option represent
the concepts of having something
or having nothing.
If a function returns a value of
type Option, you'll either get
a Some variant
with a value inside,
or you'll get a None variant without
any value because there wasn't anything to
return.
For example, vectors have a
last method that returns an option.
This code will demonstrate how last works.
In the vector named nonempty_list
here, we have a vector
with elements and we're printing out the debug
format of the return value.
We also have an empty_list that we
call last on
and print.
If we run this code, we see
that if the vector has elements,
last returns the Some variant containing
the last element in the vector.
If the vector doesn't have elements,
the last method returns the None variant.
So, once we have a Result
or Option, what do we do with them?
Let's start by looking at the success
case.
If we have a value inside Result's
Ok variant
or Option's Some variant, we
can't use it directly as if it was just
the value - the types don't match.
For example, if we've parsed a JSON
string into a Person instance successfully
and then try to access the name field,
we get an error that says there is no field
`name` on Result.
We need to get the value out of the Ok
variant.
One way we can do that
is by using a match expression
with an arm for the Ok variant
that captures the value inside it,
which we've done here
with a variable named inner.
We then return that value out of
the match
and assign it to the variable first_inner.
And then we can use the person value
we've extracted.
Now, let's talk about what to do in
the failure case.
We can specify the error variant in
another arm of the match expression,
or the None variant
if we have an Option,
and the code we put in that arm depends
on what we want to do
in case of a problem.
For example, trying to parse
a JSON string
with a trailing comma will put us in
the Err case where we could choose
to return a default value.
We could also choose to handle different
errors differently.
We do that by capturing the inner
Err information by using a variable
instead of an underscore.
And then in the code block, we can take
different actions based on the properties
of the error.
We saw earlier that this call
to from_serde
returns a serde_json error.
This Error struct has a number of
methods on it to get more information
about the error so we can decide what
to do about it.
For example, this is_eof
method is useful if we're attempting
to process streaming data.
If that's the case, we could try
to recover by reading more data
and retrying.
Finally, let's talk about a choice
you always have - turning a
recoverable error into an unrecoverable
one.
In the match expression arm, where we're
handling an Err variant,
we can choose to call the panic! macro to
end execution for all the
reasons we learned about in the previous module.
It's not possible to turn an
unrecoverable error into a
recoverable one. Therefore,
Result gives callers the most flexibility.
If the match expressions we used in this
module
look messy and verbose to you,
keep watching! In a few modules
from now, we'll be looking at some ways
to improve this code.
The improvements are variations on
the behavior of this match expression, though,
so it's important to understand this basic
case.
In this module,
we discussed the Result enum
that has the variants Ok and Err
to represent success
and failure,
and the Option enum that has the variants
Some and None to represent
something or nothing.
We looked at examples using a match
expression to extract the value
in the success case
and attempt to recover in the failure case
by taking an action like using a default
value or retrying.
We also talked about using the panic! macro
within the match to turn a recoverable
error into an unrecoverable one
if we want.
Next, let's look at returning Result
from functions and using the ?
operator.