Welcome to module 6 on custom
error types.
In this module, we're going to cover
two solutions to the problem of a
function that might fail in multiple
ways.
We'll first talk about how
and why to use Box
as the error type of a function-returning
result.
Then, we'll talk about when
Box might not be sufficient,
and how to use a custom error type
instead.
When using a custom error type,
you'll need to implement the Error trait.
Implementing the From trait will let
you use the ? operator.
We'll show you how to implement those.
Finally, we'll show a common
idiom defining a Result
type alias that uses your custom error.
Let's get started
with how and why
to return Box.
In the module on advantages of Rust's
error handling strategy,
we ended with a function that reads an
environment variable that parses
the environment variable string value into a
number and adds 1 to it.
We want to use the ? operator
to propagate the errors,
but the problem is each of these operations
returns a different error type.
Therefore, we can't use either
of those types as the error type
for our function.
So, what can we use?
One solution is specifying
the trait object, Box,
using the Error trait
from the Standard Library.
Let's talk in detail about what
Box means.
You might see this written as
Box in versions
of Rust greater than 1.27 -
they're the same thing.
This is the simplest solution to
the problem we had in the num_threads
function. The function will now
compile.
Box is a trait object.
A trait object consists of a pointer,
in this case the Box type, which points
to data on the heap; and
a trait,
in this case, the Standard Library's Error trait.
See The Rust Programming Language book,
chapter 17, for more
information on trait objects in general.
What this type means is that this
function will return some type that
implements the Error trait,
but the program doesn't know what the concrete type
will be until runtime.
The Error trait requires the Debug
and Display traits.
So, what functions that call this function
know is that the error value returned
will be something that can be printed either
to a developer
or an end user.
For example, here's the main
function for a command-line program
that has a wrapper for the entire application's
functionality - a function called
run_application.
run_application calls the
num_threads function
and anything else the application does.
If num_threads returns an error
that gets propagated up,
main panics
with a message that displays the error,
then exits.
For an application
with an outer layer that handles all
errors by printing them for
the end user, the
Box strategy is all you need.
The main downside to using Box
is that code that calls a function returning
Box can't see the
actual type that was returned
and use that information to do something
different.
If that's the case, you should instead
use a custom error type.
Let's look at how
and why to do that.
For this example, we're going to
implement a tiny part of a
document storage service, creating
a new document.
The create_document function we're implementing
will take the name of the document it should create
and will return a file for the color to
write to if everything goes well.
As implementers of the service,
we want to prevent abuse by
adding a rate limit to set how many
documents someone can create
per minute.
This function should also return an error
if a document of the specified name already
exists.
Here's a first attempt at
implementing the create_document function
using Box.
We're not going to actually implement
the rate limiting
in this example.
We've hardcoded a constant for the maximum
and the return value for a function that would
return the actual number of documents
created in the last minute.
You can change these values to simulate
being over or under the rate limit
if you want to see how this code behaves in
those cases.
The body of the create_document function
checks to see if the caller is over the rate
limit and, if so, returns
a string turned into a
Box.
This is possible because of a From trait
implementation provided by the Standard
Library.
We'll be talking about the From trait later in
this module.
Next,
the function opens a file for writing
with the create_new option set
that will return an error if the file
already exists.
If everything goes well,
this function returns a writable file
handle.
This function compiles
and does what it's supposed to do.
However, the way it's written makes
it hard for a caller to use this function.
For example, a programmer
writing an application that uses
this document creation service API
wants to wait and try again
if the rate limit has been exceeded.
If a document with that name already exists,
they'd like to add a number to the end of the name
rather than asking the end user to pick
a different name.
So, the code calling the create_document
function needs to be able to distinguish
the different failure causes, not just print
out their error.
With the create_document function as
written,
there are complex
and brittle ways callers could distinguish
the file creation errors from the string
error,
and verify that the string error text
is that of the error they're looking for.
But there's a friendlier way we could write this
function - using a custom
error type.
You can use a struct
or an enum.
An enum is more common because the type
enumerates all the ways your code
might fail.
This is friendlier because callers can
use a match expression to distinguish
between different errors.
So, to change our example from
using Box to using
a custom error type, we're first
going to define an enum named
DocumentServiceError with variants
for all the reasons our code might fail,
which is currently RateLimitExceeded
and Io.
The Io variant will hold the io error
instance we get from performing
IO operations.
Custom error types should have the Error
trait implemented on them.
So, let's do that now.
If you're using Rust 1.24.
1, as we are, implementing
the Error trait involves adding a definition
with a description method.
In Rust 1.27,
the description method on the Error trait
became optional.
So, if you're using a version of Rust newer
than 1.27, the implementation
block can be empty.
The Error trait requires that the type
also implements the Debug
and Display traits, so
we're going to derive Debug
and implement user-friendly error messages
for Display.
Next, we're going to change the return
type of the create_document function
to use DocumentServiceError instead
of Box.
Then, we change the rate limit case
to create an instance of the
RateLimit Exceeded variant
instead of a string.
In the spot where we use the ?
operator, we can leave the
code in the create_document function the
same,
if we implement the From trait.
Let's look at how to do that now.
The From trait is for converting one
type into another type.
It's useful in many situations.
The situation we're interested in right now
is that the ? operator
will look for an implementation of the
From trait to convert an error
of one type into the return
error type.
So, in order to use the ? in
our example, we need to implement
the From trait to convert from
io::Error
and create a DocumentServiceError.
The function we have to implement is also
called from, takes an
io::Error parameter
that we're going to give the name
other, and returns an instance
of DocumentServiceError,
which we can also write as Self.
In the body of the function,
we're going to wrap the io::Error
in an instance of the DocumentServiceError
Io variant.
Now, anywhere that we have an
io::Error and want to return
a DocumentServiceError,
the ? operator will take care
of the conversion for us.
This example compiles
and we've made it easier for callers to
distinguish between different ways that
our code fails so that they
can take different actions.
Finally, let's improve this code
with the Result type alias.
If our crate has many functions that
use the DocumentServiceError as
their error type, we
can reduce the repetition by defining
an alias for the Result type
within our crate that always
uses DocumentServiceError as its error
type.
This Result, as defined,
will still be generic in the success type,
so we still have to specify that
with each usage,
but the error type no longer needs to be
written out.
In this module, we examined two
solutions for dealing
with functions that return multiple error
types.
Returning the Box trait
object is sufficient in cases
where you only need to know that an error happened
and be able to print out error information.
Defining a custom error type is
recommended when callers need to be
able to distinguish in code
between different error causes.
Custom error types should implement
the Error trait, which requires
the Debug and Display traits to enable
printing.
They should implement the From trait
for any other error type that needs
to be changed into the custom error type
so that the question mark operator will take
care of the conversion.
Finally, Result type aliases
are a common way to reduce the repetition
of using the same custom error type
everywhere in a crate.
Next, let's look at a few crates
that can help when our error types become
even more complex.