Welcome to module 7 on error handling crates. In this module,
we're going to look at three error handling crates that can help as your custom error types become more complex.
We're going to use the document service example from the previous module to demonstrate each crate's features.
The crates we're going to cover are quick-error,
error-chain,
and failure.
Let's start by reviewing the document service example.
We have a DocumentServiceError enum with RateLimitExceeded and Io variants that derives Debug.
We implemented the Error trait and the Display trait on the Error enum.
We also added an implementation of the From trait to convert an io::Error to an instance of DocumentServiceError.
To better illustrate how the error handling libraries affect the code,
we're going to add a function called create_project that calls the create_document function multiple times.
Then,
to see the effects of the libraries on the code's
behavior, we'll call the create_project function from main to represent an application that uses the document service library.
The first time you run this code,
it will print Project created successfully,
and there will be four new files in your current directory.
If you run this code again, without deleting the project files,
the program will exit with an error that says File exists.
Note that the message doesn't say which file exists or which create_document call failed,
which would make it difficult to figure out why this error was happening.
This is the starting point we're going to come back to before demonstrating each crate.
So, let's take a look at what the quick-error crate does for us.
First,
let's add quick-error to Cargo.toml
version 1.2.2. To follow along, for each crate,
specify that the version should match exactly by adding the equals sign within the quotes.
If you want, when you're done with this video,
see if there's newer versions of each crate and explore how they're different.
The quick-error crate provides its functionality with a macro.
In main.rs, we bring the quick-error crate into scope along with its macros,
which is what the macro_use annotation means.
Next,
we call the quick_error! macro around the Error
enum.
The macro requires curly brackets after each variant instead of a comma and naming the data inside the Io
variant.
The macro implements the Error trait, so we can remove that implementation and the use statement.
Next,
the quick_error! macro has a less verbose way to implement the Display trait.
We can add just the text to each variant by calling what looks like a function named display.
Then, we can remove the Display implementation and the use standard format statement.
The quick_error! macro can also implement From traits. We can call what looks like a from function inside the curly braces within the Io variant definition.
This behaves the same as the original code,
but in a lot fewer lines.
Adding more error variants would be less work
too.
Next,
let's use a feature of quick-error!,
adding the filename as context for the io::Error, so we can see which file already exists.
We bring the quick_error::ResultExt trait into scope, add a file named String to the Io variant, and use the filename in the display for that variant.
Then, replace the from call with a call to context that takes a string slice for the filename and an io::Error.
Using closure-like syntax, we return a tuple of the cause and the filename converted to a string to create this variant. Finally, in create_document, we call context on the result and pass the filename. The io::Error gets past implicitly.
Now we see the name of the file that already exists.
This will let us figure out the problem faster.
Next,
let's look at what the error-chain crate provides. Starting the example back before we added the quick-error
crate,
let's add error-chain version 0.12.0 to Cargo.toml. error-chain also provides its functionality from a macro.
So, again, in main.rs, we bring the crate into our project with the macro_use annotation.
The error-chain crate recommends using the macro within an errors module.
We'll also add a use statement with a glob,
bringing everything in the errors module into the root module scope. For the rate limited error,
we define it within a block called errors
within the macro call.
error_chain! also provides a concise way to implement Display.
Coincidentally,
it's the same as with quick-error!:
specify the text in a function-like construct called display. error-chain! can implement the From trait
too.
Replace the block called foreign_links,
then define the errors with what they wrap.
Because of the way the macro works,
we have to specify the full path, starting from the root module to the
io::Error. error-chain will automatically use the Display implementation for the inner error.
The error_chain! macro provides an implementation of the Error trait on the type it defines.
So, at this point, we can remove all this code.
The error_chain! macro defines a particular type structure that it thinks works best.
There's a struct named Error that holds a variant of an enum named ErrorKind,
which is what the types we specify in the macro become. To use these types then, we need to update create_document.
error-chain! provides another macro named bail! that takes an ErrorKind and returns early with that kind in an error.
The error_chain!
macro also creates a Result type alias that uses the error it defines.
That means we can remove the one we defined and the associated use statement.
And now we're back to the same behavior as before,
adding error-chain!, with less boilerplate, in a different way from quick-error!. Now for error-chain's extra features.
error-chain! lets you add context errors by chaining errors into a list -
hence the crate's name.
For example,
to
add the filename to the io::Error,
we can call the chain_err method on the result and provide an error message containing the filename.
Now, cargo run shows the error message
we just added that contains the filename.
We can change the code in main that prints error messages to also print the chain of errors if there is one.
We skip the first error in the chain because that's the one we've already printed. Now,
cargo run
prints the full chain of errors. error-chain! can also provide more information through a backtrace, like what you would get with a panic!.
In main,
we can print the backtrace as well.
The backtrace method returns an Option because the error might not have a backtrace.
Just using cargo run won't display the backtrace. Backtraces are only captured when the environment variable, RUST_BACKTRACE, is set to anything other than zero.
Here, we've set the environment variable and can see where in our code the error came through.
This can really help track down why errors are happening.
The last crate we'll look at is the failure crate.
Let's put failure version 0.1.5 and failure_derive version 0.1.5 in Cargo.toml. failure provides a procedural macro that lets you derive an implementation for the Fail trait.
In main.rs,
we need to bring the two crates into scope with macro_use on failure_derive.
Then, we can derive the Fail trait on the Error
enum.
The failure crate implements the standard Error trait for everything that implements the Fail trait, so we can remove the implementation of the Error trait and the associated use statement.
The failure_derive
crate,
also helps implement Display.
We add the fail attribute with the argument named display set to the string to show. We can also refer to data within the variants using an underscore and the field index.
And at this point,
we no longer need the Display implementation or the associated use statement.
failure also includes backtrace support. To add backtraces to all of our error variants,
we need to bring failure, Backtrace, and Fail into our project's scope.
Then, we store a backtrace in each variant and create a new Backtrace instance
whenever we create a new error instance. In main,
we print the backtrace
if this error has a backtrace, and if a backtrace was generated. failure's
backtrace also won't display if we use cargo run.
When we set RUST_BACKTRACE,
we get useful information about where in our code the error came from.
So, which of these error crates should you use in your project?
It depends - mostly on which features you need and how you'd like to work with the crates.
Here's a handy table showing the unique aspects of each crate to consider.
You should try them out in your own projects.
In this module,
we covered three error handling crates and looked at how to use them in our document service
example
to eliminate boilerplate and provide extra features.
We saw how the quick-error crate provides a macro that helps to implement the display in From traits and provides a way to add context.
The error-chain crate has the macro that defines error type structures that let you chain errors together and that have backtrace support. The failure and failure_derive
crates provide derive macros to make implementing the traits easier and also provide backtrace support.
Next,
let's look at useful methods on Result and Option.