The evil crate đ
This crate lets you use the ? operator as a shorthand for .unwrap(). Works on both Result and Option!
= "0.2"
Example
The evil crate significantly reduces boilerplate in tests. Error handling in tests dilutes the substance of your test.
By removing all that boilerplate, you are now free to spend your energy and focus on what you are actually testing.
Before
After
Use evil::Result<()> as the return type of your test functions:
Each one of those ? is equivalent to a .unwrap().
Use the evil crate in scripts
When writing small Rust scripts that will only be used by developers, .unwrap()ping everything instead of proper error handling is common.
But there is one huge disadvantage with that approach.
Scripts turn into programs much more often than weâd like. Then, refactoring all of that .unwrap() boilerplate into good error handling is a significant undertaking.
If you use evil::Result<()> from the get-go, later refactoring your script to use something like anyhow::Result<()> is much simpler - youâre
already using the ? operator everywhere anyway. Itâs a piece of cake.
Why should I use evil::Result<()> instead of eyre::Result<()>?
The benefits of unwrapping everything is that you get the exact file, line and column information on where the unwrap failed. Thatâs amazing. It helps debugging tremendously.
When returning Result<(), Box<dyn core::error::Error>> from your function, you donât get that. That information is simply discarded. Good luck figuring out where the error came from if you just use ?. When returning anyhow::Result<()>, itâs the same problem.
But eyre::Result<()> is built different. It is special.
eyre::Result<()> actually tells you the file, line and column information of where you use the ? operator. But it has one huge downside compared to evil::Result<()>: It only works on Results, not Options.
Letâs come back to our example and rewrite it with eyre:
use OptionExt as _;
This is even more verbose than just using .unwrap()s. At least when unwrapping, you donât have to think about why each individual Option is actually always Some.
You want to think about the substance of your test, not error handling boilerplate
Wow, the evil crate is so cool! But Nightly Rust?
This crate requires nightly rust, because customizing behavior of the ? operator requires the Try trait.
But hold on! That does not mean your project needs to have a nightly MSRV (Minimum Supported Rust Version).
Your test suiteâs MSRV can be nightly, but your projectâs MSRV can be a stable Rust version. Tests arenât shipped to your users, so youâre free to improve
your developer experience writing them as much as youâd like.
When developing my Rust projects, I always have a rust-toolchain.toml that uses nightly Rust:
= "nightly"
Then, in Cargo.toml, I set a stable MSRV:
[]
= "1.90"
Now, all the Nightly Rust components will be used for tests. You get to use unstable features in tests all the time, while having the actual project build using Stable Rust. You get faster compile speeds. You get to use nightly rustfmt options like wrap_comments, format_code_in_doc_comments and imports_granularity = "Item" for way less merge conflicts. Nightly compile speeds are faster, itâs amazing for developing.
But when it comes to shipping the code to users, the actual code will build on Stable Rust and not use any unstable features. I use cargo hack in GitHub Actions CI to check that my project always builds with my MSRV:
# This GitHub action runs on every commit to the `main` branch,
# and also on every Pull Request
name: Check
on:
pull_request:
push:
branches:
- main
jobs:
cargo-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
- uses: taiki-e/install-action@cargo-hack
- run: cargo hack check --each-feature --locked --rust-version --ignore-private --workspace --lib --bins --keep-going
How does it work?
The ? operator is syntax sugar for the Try trait. This expression:
let html = fetch?;
Desugars to the following:
let html = match branch ;
Whatever fetch() returns, it must implement the Try trait. Assume that fetch() returns Result<String, HtmlError>, then Try::branch is this function:
< as Try>branch
This implementation comes from the standard library. The function branch is this:
Now, consider if fetch() returned an Err(HtmlError). That means branch returned ControlFlow::Break(Err(e)). So our match becomes:
let html = match Break ;
This hits the 2nd arm, and evaluates to this:
let html = return from_residual;
We hit an error, and we do an early return. This is the short-circuiting behavior of the ? operator.
The FromResidual is a helper trait which tells us what exactly to return. The value of the expression FromResidual::from_residual(r) is determined by type inference.
Letâs say we are inside of a function that returns evil::Result:
The type of r is Result<!, HtmlError>. This is the âresidualâ, it is essentially an âalways-failâ version of a type implementing Try:
- For
Option<T>, this isOption<!>, which is always justOption::None- becauseNoneis considered the failure case of anOption. - For any
Result<T, E>, it is alwaysResult<!, E>becauseResult::Erris the failure case of aResult
So r has type Result<!, HtmlError> and expression FromResidual::from_residual(r) must have type evil::Result<()>
This is the implementation that gets used:
Because Result<!, HtmlError> is always an Err, we can just infallibly get the Err value:
let Err = residual;
Usually, this from_residual would actually return Self here - so we would return from the function process_webpage.
But the way that evil::Result implements Try is such that panic!() will be called instead:
panic!