snafu
repository·main·Indexed 23 days ago
https://github.com/shepmaster/snafuAn ergonomic error handling library for Rust (version 0.9.2) designed to simplify mapping low-level errors into domain-specific error types. It provides the `Snafu` derive macro for custom error enums, context selectors for attaching metadata, and utilities like `ensure!` and `.context()`. The library also supports asynchronous error handling via `TryFutureExt` and `TryStreamExt`, and offers a `Whatever` type for simple string-based errors.
What's inside snafu
- To maintain high cohesion and reduce complexity when matching errors, SNAFU encourages defining one or more error types scoped specifically to each module. This prevents a single, monolithic error type from containing unrelated errors from across the entire project, making it easier for consumers to handle errors relevant only to the module they are interacting with.
Implement opaque error types with an `ErrorKind`
mainTo hide implementation details from users, you can use an opaque error pattern. This involves wrapping a private
InnerErrorenum inside a publicstruct Error.You can then implement methods on the public
Errorstruct to expose specific information or a high-levelErrorKindenum. This allows users to match on the error's category without needing to know the internal structure of your error variants.use snafu::prelude::*; #[derive(Debug, Snafu)] enum InnerError { MyError1 { username: String }, MyError2 { username: String }, MyError3 { address: String }, } #[derive(Debug, Snafu)] pub struct Error(InnerError); #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum ErrorKind { Authorization, Network, } impl Error { pub fn kind(&self) -> ErrorKind { use InnerError::*; match self.0 { MyError1 { .. } | MyError2 { .. } => ErrorKind::Authorization, MyError3 { .. } => ErrorKind::Network, } } pub fn username(&self) -> Option<&str> { use InnerError::*; match &self.0 { MyError1 { username } | MyError2 { username } => Some(username), _ => None, } } }How context selectors work in SNAFU
mainWhen you use
#[derive(Snafu)]on an enum, the macro generates a specialized struct for each variant called a context selector.If you define a variant like this:
#[derive(Debug, Snafu)] pub enum ProjectError { IOConfigError { path: &'static str, source: io::Error, }, }The macro generates a struct named
IOConfigSnafu. This selector is designed to be used with the.context()method. It allows you to provide only the additional fields (likepath) while thesourcefield is automatically captured from the error being wrapped.Using the enum variant
ProjectError::IOConfigErrordirectly in.context()fails because the variant expects all fields (includingsource) to be provided manually, whereas the context selectorIOConfigSnafuis specifically built to handle theIntoErrortrait implementation required by SNAFU's context pattern.Understand default Display behavior in SNAFU
mainIf you do not provide a
#[snafu(display(...))]attribute, SNAFU determines theDisplayimplementation using the following priority:- The summary of the variant's documentation comment (doc comment).
- The name of the variant.
# use snafu::prelude::*; #[derive(Debug, Snafu)] enum Error { /// No user available. /// You may need to specify one. MissingUser, MissingPassword, } fn main() { // Uses the doc comment assert_eq!( MissingUserSnafu.build().to_string(), "No user available. You may need to specify one.", ); // Uses the variant name assert_eq!(MissingPasswordSnafu.build().to_string(), "MissingPassword"); }# use snafu::prelude::*; #[derive(Debug, Snafu)] enum Error { /// No user available. /// You may need to specify one. MissingUser, MissingPassword, } fn main() { assert_eq!( MissingUserSnafu.build().to_string(), "No user available. You may need to specify one.", ); assert_eq!(MissingPasswordSnafu.build().to_string(), "MissingPassword"); }Categorize underlying errors by their context
mainWhen using SNAFU, you should aim to wrap low-level, generic errors (likestd::io::Error) into domain-specific error types. This allows you to bin a single underlying error type into multiple distinct errors that reflect the specific context of your application or module, while optionally attaching additional contextual information to the error.Use transparent errors to delegate Display and source
mainUse
#[snafu(transparent)]to delegate both theDisplayandError::sourceimplementations to an underlying error. This is useful for composing error types without creating redundant nesting in the error chain orDisplayoutput.Note:
#[snafu(transparent)]implies#[snafu(context(false))]. Because it delegatesDisplayto the source, you cannot use#[snafu(display(...))]on transparent variants.# use snafu::prelude::*; # fn add_to_group(group: u32, user: &str) -> Result<(), AddToGroupError> { let group = GroupId::validate(group)?; // ... do useful operation Ok(()) } fn remove_from_group(group: u32, user: &str) -> Result<(), RemoveFromGroupError> { let group = GroupId::validate(group)?; // ... do useful operation Ok(()) } #[derive(Debug, Snafu)] enum AddToGroupError { #[snafu(transparent)] Group { source: GroupIdError }, // ... other failure conditions } #[derive(Debug, Snafu)] enum RemoveFromGroupError { #[snafu(transparent)] Group { source: GroupIdError }, // ... other failure conditions } #[derive(Debug)] struct GroupId(u32); impl GroupId { fn validate(id: u32) -> Result<Self, GroupIdError> { // ... perform validation # GroupIdSnafu { id }.fail() } } #[derive(Debug, Snafu)] #[snafu(display("Group ID {id} does not exist"))] struct GroupIdError { id: u32 };Understand the code generated by the `Snafu` macro
mainThe
#[derive(Snafu)]procedural macro automatically implements several traits and generates helper types to simplify error handling. When applied to an enum, it produces:- Context Selectors: Helper structs used to construct error variants. They use generic types for fields you must provide and automatically handle
sourceandbacktracefields. Errortrait implementation: Implementsstd::error::Error, providingsource()to return underlying errors andcause()(aliased tosource()).Displaytrait implementation: Implementsstd::fmt::Displayusing the format string provided in the#[snafu(display("..."))]attribute. If no format is provided, the variant name is used.ErrorCompattrait implementation: Provides a way to retrieve aBacktraceif the variant includes abacktracefield.
Note: The exact generated code may vary. Use
cargo-expandto inspect the precise output for your specific implementation.use snafu::{prelude::*, Backtrace}; use std::path::PathBuf; #[derive(Debug, Snafu)] enum Error { #[snafu(display("Could not open config at {}", filename.display()))] OpenConfig { filename: PathBuf, source: std::io::Error, }, #[snafu(display("Could not open config"))] SaveConfig { source: std::io::Error }, #[snafu(display("The user id {user_id} is invalid"))] UserIdInvalid { user_id: i32, backtrace: Backtrace }, #[snafu(display("Could not validate config with key {key}: checksum was {checksum}"))] ConfigValidationFailed { checksum: u64, key: String, source: crypto::Error, }, } mod crypto { #[derive(Debug, snafu::Snafu)] pub struct Error; }- Context Selectors: Helper structs used to construct error variants. They use generic types for fields you must provide and automatically handle
Associate arbitrary data with errors using the Provider API
mainWhen the
unstable-provider-apifeature flag is enabled, errors implement theError::providemethod. This allows you to associate arbitrary data with an error instance, which can then be retrieved by consumers usingcore::error::request_reforcore::error::request_value.To expose a field as data, use the
#[snafu(provide)]attribute on that field. This makes the field available viarequest_ref.Note: Using this attribute without the
unstable-provider-apiflag is safe; the attribute will be parsed but no code will be generated, allowing library authors to support both stable and nightly Rust users.use core::error; use snafu::prelude::*; #[derive(Debug)] struct UserId(u8); #[derive(Debug, Snafu)] enum ApiError { Login { #[snafu(provide)] user_id: UserId, }, Logout { #[snafu(provide)] user_id: UserId, }, NetworkUnreachable { source: std::io::Error }, } let e = LoginSnafu { user_id: UserId(0) }.build(); match error::request_ref::<UserId>(&e) { // Present when ApiError::Login or ApiError::Logout Some(UserId(user_id)) => { println!("{user_id} experienced an error"); } // Absent when ApiError::NetworkUnreachable None => { println!("An error occurred for an unknown user"); } }How context selectors work
mainFor every enum variant,
Snafugenerates a context selector struct. This struct allows you to build error variants by providing only the necessary context.Naming and Structure
- Name: The variant name with the suffix
Snafuadded. If the variant name ends inError, that suffix is removed. - Fields: Each field in the variant (except
sourceandbacktrace) is replaced with a generic type in the selector. - Automatic Fields:
sourceandbacktraceare omitted from the selector's fields because the library handles them automatically.
Usage Patterns
Variants with a
sourcefieldIf a variant has a
sourcefield, the selector implementsIntoError<Error>. This allows you to convert a source error into your custom error type using the?operator or.into().Variants without a
sourcefieldIf there is no
sourcefield, the selector providesbuild()andfail()methods. These can be used with theensure!macro.If the original variant had a
backtracefield, the backtrace is automatically constructed when callingIntoError,build, orfail.- Name: The variant name with the suffix
Upgrade from 0.4 to 0.5: `backtrace` attribute simplification
mainIn version 0.5, the
#[snafu(backtrace(delegate))]attribute was replaced by the simpler#[snafu(backtrace)]for source fields that reference other errors.// Before #[derive(Debug, Snafu)] enum Error { MyVariant { #[snafu(backtrace(delegate))] source: OtherError, }, } // After #[derive(Debug, Snafu)] enum Error { MyVariant { #[snafu(backtrace)] source: OtherError, }, }Upgrade from 0.6 to 0.7: Minimum Rust version
mainAs of version 0.7, the minimum supported Rust version is 1.34.Upgrade from 0.4 to 0.5: `source(from)` implies `source`
mainIn version 0.5, using
#[snafu(source(from(...)))]now automatically implies#[snafu(source)]. You no longer need to specify both attributes on a field to treat it as a source field with transformation.// Before #[derive(Debug, Snafu)] enum Error { CauseIsAnError { #[snafu(source)] #[snafu(source(from(Error, Box::new)))] cause: Box<Error>, }, } // After #[derive(Debug, Snafu)] enum Error { CauseIsAnError { #[snafu(source(from(Error, Box::new)))] cause: Box<Error>, }, }