To be able to insert Pokemon data we'll need a connection to the database. Inside of our main function, we can use std::env::var
to get the DATABASE_URL
we're already using when we cargo check
or cargo run
.
env::var
returns a Result
because it can fail. We can't continue without a database connection so we can use ?
to unwrap the Result
and return an error immediately if one occurs.
fn main() -> Result<(), csv::Error> {
let database_url = env::var("DATABASE_URL")?;
...
}
Since we've used env::var
in the body of our main function, we'll have to bring the env
module into scope as well.
use std::env;
Running this nets us new errors!
error[E0277]: `?` couldn't convert the error to `csv::Error`
--> crates/upload-pokemon-data/src/main.rs:8:48
|
7 | fn main() -> Result<(), csv::Error> {
| ---------------------- expected `csv::Error` because of this
8 | let database_url = env::var("DATABASE_URL")?;
| ^ the trait `From<VarError>` is not implemented for `csv::Error`
|
= note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait
= help: the trait `From<std::io::Error>` is implemented for `csv::Error`
= note: required for `Result<(), csv::Error>` to implement `FromResidual<Result<Infallible, VarError>>`
The type of the return value of our main function is Result<(), csv::Error>
which worked well when we only had csv::Error
s occuring. Now we have std::env::VarError
s as well (and soon we'll have sqlx::Error
s too!).
There are two general approaches we can take here. We can either forward errors to the user or we can construct a structured error type that contains everything that can go wrong in our program and use that error as our return type instead.
Option 2, building up our own error type, makes a lot more sense for a library because people who use our crate would want to do different things in code based on which error was returned.
For us in this application, we don't actually care about handling most of the errors and the most useful thing we can do is report those errors to the person running the CLI. So dealing with the extra machinery of constructing an error type is wasted effort (unless we wanted to add help text to specific cases).
There are a few libraries that can help us deal with errors on the application level like this: anyhow
, eyre
, and miette
are all fairly interchangable options with anyhow
being the oldest implementation and miette
being the newest.
We'll use miette
because while it is newer it is quickly gaining adoption for good reason.
...but to be clear the libraries I just mentioned are all fairly interchangeable for this use case and you can play with a few to see how they work if you want to.
We'll add miette to our crate and enable the fancy
feature, which enables some nicer visuals since this is an application and not a library.
cargo add miette -p upload-pokemon-data -F fancy
Change the return type of our main function to use the new Result
type from miette
. This includes a default error type called Report
.
fn main() -> miette::Result<()> {..}
miette will handle displaying our errors nicely, but to do so it uses an additional trait: Diagnostic
.
Diagnostic
adds a whole bunch of information about our errors and uses that information to report about them to the user. However, most errors we come across will implement the Error
trait instead.
miette exposes another trait called IntoDiagnostic
that allows us to easily convert any type that implements Error
into a Report
. We can use this function (into_diagnostic
) on the 3 errors we have in main.rs
as long as we bring miette::IntoDiagnostic
into scope.
This results in this code in src/main.rs
.
mod pokemon_csv;
use miette::IntoDiagnostic;
use pokemon_csv::*;
use std::env;
mod db;
use db::*;
fn main() -> miette::Result<()> {
let database_url =
env::var("DATABASE_URL").into_diagnostic()?;
let mut rdr = csv::Reader::from_path(
"./crates/upload-pokemon-data/pokemon.csv",
)
.into_diagnostic()?;
for result in rdr.deserialize() {
let record: PokemonCsv =
result.into_diagnostic()?;
let pokemon_row: PokemonTableRow = record.into();
dbg!(pokemon_row);
}
dbg!(PokemonId::new());
Ok(())
}
and now if we cargo run
with our DATABASE_URL
set, everything works and our errors get propogated up to the new report.
DATABASE_URL=mysql://127.0.0.1 cargo run
We can confirm that our errors get returned to the user by taking the environment variable away
Note: you must have run cargo build or cargo run with the DATABASE_URL set already because we use the environment variable at compile time to check that our sqlx queries are valid and this error is a runtime check to make sure someone set the environment variable. cargo run will run build for you if you haven't run it already and you will see the compilation error, not the runtime error.
cargo run
which returns
Error: × environment variable not found
But that's not a terribly useful error message.
Wrapping errors with miette
Miette allows us to take two actions to improve our error messages here.
First: we can replace the "environment variable not found" message with something that mentions what environment variable wasn't found.
Second: we can add some help text to suggest that the user do something to solve this error.
We'll need to bring some items into scope:
miette::miette
which is a macro that allows us to construct arbitrary errors, and in doing so specify additional information like help text.miette::WrapErr
which allows us to wrap an error with additional context about why the error has occurred.
We'll bring the miette
and WrapErr
items into scope.
use miette::{miette, IntoDiagnostic, WrapErr};
and change our database_url
logic:
let database_url = env::var("DATABASE_URL").map_err(|e| {
miette!(
help="Run `pscale connect <database> <branch>` to get a connection",
"{e}"
)
})
.wrap_err("Must have a DATABASE_URL set")?;
After running cargo build with the database url, and cargo run
without the database_url, we see the following error message, which displays our new error message, the error as it existed, and our help text.
Error: × Must have a DATABASE_URL set
╰─▶ environment variable not found
help: Run `pscale connect <database> <branch>` to get a connection
Since env::var
returns a Result<String, VarError>
, we can use Result::map_err
to change the error type.
We do this by constructing a new error using the miette
macro. The miette
macro allows us to set additional fields and dynamically supply Diagnostic-like arguments, so we set the help
text and we pass the original error through using the second argument. The second argument is a formatting string, so what we're doing here is saying "use the display formatter to represent the original error: e
"
This returns a Result<String, Report>
from map_err
, which we can then use wrap_err
on to add additional context.
With the database url in place and error reporting set up we can continue to set up a database connection.