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::Errors occuring. Now we have std::env::VarErrors as well (and soon we'll have sqlx::Errors 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::miettewhich is a macro that allows us to construct arbitrary errors, and in doing so specify additional information like help text.miette::WrapErrwhich 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.