Build with Naz : Rust error handling with miette
- Introduction
- Rust error handling primer
- More resources on Rust error handling
- YouTube video for this article
- Examples of Rust error handling with miette
- Build with Naz video series on developerlife.com YouTube channel
Introduction #
miette is an excellent crate that can make error handling in Rust powerful, flexible, and easy to use. It provides a way to create custom error types, add context to errors, and display errors in a user-friendly way. In this article, video, and repo, weโll explore how to use miette to improve error handling in your Rust applications.
Rust error handling primer #
Rust has a powerful error handling system that is based on the
Result and Option types. For this tutorial we
will focus on the Result type, which is an enum that has two variants: Ok and Err.
The Ok variant is used to represent a successful result, while the Err variant is used
to represent an error.
The Error trait in Rust has to
be implemented for types that can be used as errors. The Error trait has a method called
source that returns a reference to the underlying cause of the error. This trait has two
supertraits: Debug and Display. The Debug trait is used to format the error for
debugging purposes (for the operator), while the Display trait is used to format the
error for displaying to the user.
The ? operator can be used in order to propagate errors up the call stack. This operator
is used to unwrap the Result type and provide the inner value of the Ok variant.
Otherwise it returns from the function with the error, if it is the Err variant. This
operator can only be used in functions that return a Result type. Hereโs an example:
/// Fails and produces output:
/// ```text
/// Error: ParseIntError { kind: InvalidDigit }
/// ```
#[test]
fn test() -> Result<(), Box<dyn std::error::Error>> {
fn return_error_result() -> Result<u32, std::num::ParseIntError> {
"1.2".parse::<u32>()
}
fn run() -> Result<(), Box<dyn std::error::Error>> {
// It is as if the `?` is turned into the following code.
// let result = match result {
// Ok(value) => value,
// Err(err) => return Err(Box::new(err)),
// }
let result = return_error_result()?;
// The following lines will never be executed, since the previous
// line will return from the function with an error.
println!("Result: {}", result);
Ok(())
}
run()?;
Ok(())
}
In the rest of the tutorial (and accompanying video), we will build upon this knowledge
and introduce miette, a crate that can make error handling in Rust powerful, flexible,
and easy to use. We will also learn more about the thiserror crate, which can be used to
easily create custom error types in Rust.
More resources on Rust error handling #
YouTube video for this article #
This blog post has short examples on how to use miette to enhance Rust error handling. If
you like to learn via video, please watch the companion video on the developerlife.com
YouTube channel.
Examples of Rust error handling with miette #
Letโs create some examples to illustrate how to use miette to enhance Rust error
handling. You can run cargo new --lib error-miette to create a new library crate.
The code in the video and this tutorial are all in this GitHub repo.
Then add the following to the Cargo.toml file thatโs generated. These pull in all the
dependencies that we need for these examples.
[package]
name = "error-miette"
version = "0.1.0"
edition = "2021"
[dependencies]
# Pretty terminal output.
crossterm = "0.27.0"
# Error handling.
thiserror = "1.0.61"
miette = { version = "7.2.0", features = ["fancy"] }
pretty_assertions = "1.4.0"
Example 1: Simple miette usage #
Then you can add the following code to the src/lib.rs file. You can note the following things
in the code:
- We define a custom error type called
UnderlyingDatabaseErrorusing thethiserrorcrate. - We define a function called
return_error_resultthat returns aResult<u32, std::num::ParseIntError>. - We write a test called
test_into_diagnosticthat demonstrates how to usemietteto add context to errors and display them in a user-friendly way. The test also demonstrates how to use thewrap_errandcontextmethods to add context to errors. And how they are displayed in the error report (in the inverse order in which they were added). - We also demonstrate how to use the
into_diagnosticmethod to convert aResultinto amiette::Result.
#[cfg(test)]
pub mod simple_miette_usage {
use crossterm::style::Stylize;
use miette::{Context, IntoDiagnostic};
#[derive(Debug, thiserror::Error)]
pub enum UnderlyingDatabaseError {
#[error("database corrupted")]
DatabaseCorrupted,
}
fn return_error_result() -> Result<u32, std::num::ParseIntError> {
"1.2".parse::<u32>()
}
#[test]
fn test_into_diagnostic() -> miette::Result<()> {
let error_result: Result<u32, std::num::ParseIntError> =
return_error_result();
assert!(error_result.is_err());
// The following line will return from this test.
// let it: u32 = error_result.into_diagnostic()?;
let new_miette_result: miette::Result<u32> = error_result
.into_diagnostic()
.context("๐ foo bar baz")
.wrap_err(miette::miette!("custom string error"))
.wrap_err(std::io::ErrorKind::NotFound)
.wrap_err(UnderlyingDatabaseError::DatabaseCorrupted)
.wrap_err("๐ this is additional context about the failure");
assert!(new_miette_result.is_err());
println!(
"{}:\n{:?}\n",
"debug output".blue().bold(),
new_miette_result
);
if let Err(ref miette_report) = new_miette_result {
println!(
"{}:\n{:?}\n",
"miette report".red().bold(),
miette_report.to_string()
);
let mut iter = miette_report.chain();
// First.
pretty_assertions::assert_eq!(
iter.next().unwrap().to_string(),
"๐ this is additional context about the failure"
.to_string()
);
// Second.
pretty_assertions::assert_eq!(
iter.next().unwrap().to_string(),
"database corrupted".to_string()
);
// Third.
pretty_assertions::assert_eq!(
iter.next().unwrap().to_string(),
"entity not found".to_string()
);
// Fourth.
pretty_assertions::assert_eq!(
iter.next().unwrap().to_string(),
"custom string error".to_string()
);
// Fifth.
pretty_assertions::assert_eq!(
iter.next().unwrap().to_string(),
"๐ foo bar baz".to_string()
);
// Final.
pretty_assertions::assert_eq!(
iter.next().unwrap().to_string(),
"invalid digit found in string".to_string()
);
}
Ok(())
}
#[test]
fn test_convert_report_into_error() ->
std::result::Result<(), Box<dyn std::error::Error>> {
let miette_result: miette::Result<u32> =
return_error_result()
.into_diagnostic()
.wrap_err(miette::Report::msg(
"wrapper for the source parse int error",
));
// let converted_result: Result<u32, Box<dyn Error>> =
// miette_result.map_err(|report| report.into());
let converted_result:
std::result::Result<(), Box<dyn std::error::Error>> =
match miette_result {
Ok(_) => Ok(()),
Err(miette_report) => {
let boxed_error: Box<dyn std::error::Error> =
miette_report.into();
Err(boxed_error)
}
};
println!(
"{}:\n{:?}\n",
"debug output".blue().bold(),
converted_result
);
assert!(converted_result.is_err());
Ok(())
}
}
Example 2: Complex miette usage #
Next, we will add the following code to the src/lib.rs file. You can note the following
things in the code:
- We define a custom error type called
KvStoreErrorusing thethiserrorcrate. - We define two variants for the
KvStoreErrorenum:CouldNotCreateDbFolderandCouldNotGetOrCreateEnvOrOpenStore. The latter variant has a field calledsourcethat is of typeUnderlyingDatabaseError, which is defined in the previous example. - We define two functions called
return_flat_errandreturn_nested_errthat returnmiette::Result<(), KvStoreError>. - We write two tests called
fails_with_flat_errandfails_with_nested_errthat demonstrate how to usemietteto add context to errors and display them in a user-friendly way. The tests also demonstrate how to use thefromattribute to convert an error of one type into an error of another type. - We also demonstrate how to use the
#[diagnostic]attribute to add a code and help URL to the error type. - We also demonstrate how to use the
#[from]attribute to convert an error of one type into an error of another type. - We also demonstrate how to use the
#[error]attribute to add a custom error message to the error type.
#[cfg(test)]
pub mod complex_miette_usage {
use std::error::Error;
use crate::simple_miette_usage::UnderlyingDatabaseError;
use pretty_assertions::assert_eq;
#[derive(thiserror::Error, Debug, miette::Diagnostic)]
pub enum KvStoreError {
#[diagnostic(
code(MyErrorCode::FileSystemError),
help("https://docs.rs/rkv/latest/rkv/enum.StoreError.html"),
// url(docsrs) /* Works if this code was on crates.io / docs.rs */
)]
#[error("๐ Could not create db folder: '{db_folder_path}' on disk")]
CouldNotCreateDbFolder { db_folder_path: String },
#[diagnostic(
code(MyErrorCode::StoreCreateOrAccessError),
help("https://docs.rs/rkv/latest/rkv/enum.StoreError.html"),
// url(docsrs) /* Works if this code was on crates.io / docs.rs */
)]
#[error("๐พ Could not get or create environment, or open store")]
CouldNotGetOrCreateEnvOrOpenStore {
#[from]
source: UnderlyingDatabaseError,
},
}
fn return_flat_err() -> miette::Result<(), KvStoreError> {
Result::Err(KvStoreError::CouldNotCreateDbFolder {
db_folder_path: "some/path/to/db".to_string(),
})
}
/// This test will not run! It will fail and demonstrate the default
/// [report handler](miette::ReportHandler) of the `miette` crate.
#[test]
fn fails_with_flat_err() -> miette::Result<()> {
let result = return_flat_err();
if let Err(error) = &result {
assert_eq!(
format!("{:?}", error),
"CouldNotCreateDbFolder { db_folder_path: \"some/path/to/db\" }"
);
}
result?;
Ok(())
}
fn return_nested_err() -> miette::Result<(), KvStoreError> {
// Variant 1 - Very verbose.
let store_error = UnderlyingDatabaseError::DatabaseCorrupted;
let rkv_error = KvStoreError::from(store_error);
Result::Err(rkv_error)
// Variant 2.
// Result::Err(KvStoreError::CouldNotGetOrCreateEnvOrOpenStore {
// source: UnderlyingDatabaseError::DatabaseCorrupted,
// })
}
/// This test will not run! It will fail and demonstrate the default
/// [report handler](miette::ReportHandler) of the `miette` crate.
#[test]
fn fails_with_nested_err() -> miette::Result<()> {
let result = return_nested_err();
if let Err(error) = &result {
assert_eq!(
format!("{:?}", error),
"CouldNotGetOrCreateEnvOrOpenStore { source: DatabaseCorrupted }"
);
}
result?;
Ok(())
}
}
Parting thoughts #
For more sophisticated error handling examples, please check out the following links:
terminal_async.rsinr3bl_terminal_asynccrate.kv.rsintcp-api-servercrate.- Custom global report handler for
mietteintcp-api-servercrate.
Build with Naz video series on developerlife.com YouTube channel #
You can watch a video series on building this crate with Naz on the developerlife.com YouTube channel.
- YT channel
- Playlists
๐ Watch Rust ๐ฆ live coding videos on our YouTube Channel.
๐ฆ Install our useful Rust command line apps usingcargo install r3bl-cmdr(they are from the r3bl-open-core project):
- ๐ฑ
giti: run interactive git commands with confidence in your terminal- ๐ฆ
edi: edit Markdown with style in your terminalgiti in action
edi in action