TL;DR: SNAFU creates easy-to-use errors out of Rust enums using a procedural macro and extension traits.
Example
Here's the highlight of what it provides:
#[derive(Debug, Snafu)]
enum Error {
#[snafu(display("Could not validate password: {} (connected to {})", source, address))]
ValidatePassword { address: String, source: auth_server::Error }.
}
// ...
auth_server::validate(address, username, password)
.context(ValidatePassword { address })?;
Here's a complete working example with comments about the features and a few more bells-and-whistles:
mod my_library {
use snafu::{ResultExt, Snafu};
// This is an error for a public API; nothing about the details
// is exposed, just that error-related traits are implemented.
#[derive(Debug, Snafu)]
pub struct Error(InnerError);
// This is an error for an internal API; all the details are
// available for methods and tests.
#[derive(Debug, Snafu)]
enum InnerError {
// Attributes allow us to write our `Display` implementation
// concisely.
#[snafu(display("Could not validate password: {} (connected to {})", source, address))]
ValidatePassword {
address: String,
// We know that this is another SNAFU error, so we
// delegate our backtrace information to it
#[snafu(backtrace(delegate))]
source: auth_server::Error,
},
#[snafu(display("Could not load user from database: {}", source))]
LoadUser { source: database::Error },
#[snafu(display("User ID in database was invalid: {}", source))]
InvalidUserId { source: std::num::ParseIntError },
}
// This type alias makes using our error type easy, but also
// allows for one-off funnctions with a different error type.
type Result<T, E = InnerError> = std::result::Result<T, E>;
#[derive(Debug)]
pub struct User {
id: i32,
}
// This is a public function so it uses the public error type
pub fn login(username: &str, password: &str) -> Result<User, Error> {
check_password_matches(username, password)?;
let user = load_and_convert_from_db(username)?;
Ok(user)
}
const AUTH_SERVER_ADDR: &'static str = "127.0.0.1";
fn check_password_matches(username: &str, password: &str) -> Result<bool> {
let address = AUTH_SERVER_ADDR;
// We can provide additional data when we encapsulate a
// lower-level error, such as the address of the server.
let valid = auth_server::validate(address, username, password)
.context(ValidatePassword { address })?;
if !valid {
eprintln!("Bad password attempt for '{}'", username);
}
Ok(valid)
}
fn load_and_convert_from_db(username: &str) -> Result<User> {
let db_user = database::get_user(username).context(LoadUser)?;
let id = db_user.id.parse().context(InvalidUserId)?;
Ok(User { id })
}
mod auth_server {
use snafu::Snafu;
#[derive(Debug, Snafu)]
pub enum Error {
#[snafu(display("This is just a dummy error"))]
Dummy { backtrace: snafu::Backtrace },
}
pub fn validate(_address: &str, _username: &str, _password: &str) -> Result<bool, Error> {
Ok(true)
}
}
mod database {
pub struct User {
pub id: String, // We have a really bad DB schema...
}
// SNAFU interoperates with any type that implements `Error`
pub type Error = Box<dyn std::error::Error>;
pub fn get_user(_username: &str) -> Result<User, Error> {
Ok(User { id: "42".into() })
}
}
}
// There are extension traits for `Result` and `Option`
use snafu::{OptionExt, ResultExt, Snafu};
use std::io::{self, BufRead};
// SNAFU works equally well for libraries and applications
#[derive(Debug, Snafu)]
enum Error {
#[snafu(display("Could not log in user {}: {}", username, source))]
LoginFailure {
username: String,
source: my_library::Error,
},
#[snafu(display("A username is required"))]
UsernameMissing,
#[snafu(display("The username was malformed"))]
UsernameMalformed { source: io::Error },
#[snafu(display("A password is required"))]
PasswordMissing,
#[snafu(display("The password was malformed"))]
PasswordMalformed { source: io::Error },
}
fn main() -> Result<(), Error> {
let stdin = std::io::stdin();
let stdin = stdin.lock();
let mut stdin = stdin.lines().fuse();
let username = stdin
.next()
.context(UsernameMissing)?
.context(UsernameMalformed)?;
let password = stdin
.next()
.context(PasswordMissing)?
.context(PasswordMalformed)?;
let user = my_library::login(&username, &password).context(LoginFailure { username })?;
println!("Got user: {:?}", user);
Ok(())
}
How it works
The macro creates a unique type corresponding to each enum variant; I call these context selectors. These context selectors are used with the context
method to easily wrap underlying errors along with additional data into a custom error type. Attributes allow for customization of aspects like the Display
implementation.