snafu

repository·main·Indexed 23 days ago

https://github.com/shepmaster/snafu

An 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.

Tokens
23.4K
Snippets
55
Records
100
Agent score
84%

What's inside snafu

  1. Organize error types by module scope

    main
    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.
  2. Implement opaque error types with an `ErrorKind`

    main

    To hide implementation details from users, you can use an opaque error pattern. This involves wrapping a private InnerError enum inside a public struct Error.

    You can then implement methods on the public Error struct to expose specific information or a high-level ErrorKind enum. 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,
            }
        }
    }
  3. How context selectors work in SNAFU

    main

    When 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 (like path) while the source field is automatically captured from the error being wrapped.

    Using the enum variant ProjectError::IOConfigError directly in .context() fails because the variant expects all fields (including source) to be provided manually, whereas the context selector IOConfigSnafu is specifically built to handle the IntoError trait implementation required by SNAFU's context pattern.

  4. Understand default Display behavior in SNAFU

    main

    If you do not provide a #[snafu(display(...))] attribute, SNAFU determines the Display implementation using the following priority:

    1. The summary of the variant's documentation comment (doc comment).
    2. 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");
    }
  5. Categorize underlying errors by their context

    main
    When using SNAFU, you should aim to wrap low-level, generic errors (like std::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.
  6. Use transparent errors to delegate Display and source

    main

    Use #[snafu(transparent)] to delegate both the Display and Error::source implementations to an underlying error. This is useful for composing error types without creating redundant nesting in the error chain or Display output.

    Note: #[snafu(transparent)] implies #[snafu(context(false))]. Because it delegates Display to 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 };
  7. Understand the code generated by the `Snafu` macro

    main

    The #[derive(Snafu)] procedural macro automatically implements several traits and generates helper types to simplify error handling. When applied to an enum, it produces:

    1. Context Selectors: Helper structs used to construct error variants. They use generic types for fields you must provide and automatically handle source and backtrace fields.
    2. Error trait implementation: Implements std::error::Error, providing source() to return underlying errors and cause() (aliased to source()).
    3. Display trait implementation: Implements std::fmt::Display using the format string provided in the #[snafu(display("..."))] attribute. If no format is provided, the variant name is used.
    4. ErrorCompat trait implementation: Provides a way to retrieve a Backtrace if the variant includes a backtrace field.

    Note: The exact generated code may vary. Use cargo-expand to 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; }
  8. Associate arbitrary data with errors using the Provider API

    main

    When the unstable-provider-api feature flag is enabled, errors implement the Error::provide method. This allows you to associate arbitrary data with an error instance, which can then be retrieved by consumers using core::error::request_ref or core::error::request_value.

    To expose a field as data, use the #[snafu(provide)] attribute on that field. This makes the field available via request_ref.

    Note: Using this attribute without the unstable-provider-api flag 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");
        }
    }
  9. How context selectors work

    main

    For every enum variant, Snafu generates 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 Snafu added. If the variant name ends in Error, that suffix is removed.
    • Fields: Each field in the variant (except source and backtrace) is replaced with a generic type in the selector.
    • Automatic Fields: source and backtrace are omitted from the selector's fields because the library handles them automatically.

    Usage Patterns

    Variants with a source field

    If a variant has a source field, the selector implements IntoError<Error>. This allows you to convert a source error into your custom error type using the ? operator or .into().

    Variants without a source field

    If there is no source field, the selector provides build() and fail() methods. These can be used with the ensure! macro.

    If the original variant had a backtrace field, the backtrace is automatically constructed when calling IntoError, build, or fail.

  10. Upgrade from 0.4 to 0.5: `backtrace` attribute simplification

    main

    In 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,
        },
    }
  11. Upgrade from 0.4 to 0.5: `source(from)` implies `source`

    main

    In 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>,
        },
    }