#error #macro #hierarchy #reuse #shorthand #proc-macro

macro error-tree

This crate let’s us use the error_tree! proc macro for ergonomic error hierarchy definition

5 unstable releases

new 0.4.0 Nov 27, 2024
0.3.7 Oct 5, 2024
0.3.6 Jun 21, 2024
0.3.5 Jun 21, 2024
0.1.0 May 5, 2024

#20 in #shorthand

26 downloads per month
Used in 3 crates

MIT license

68KB
1K SLoC

Error Tree

error-tree is a Rust procedural macro crate designed to simplify the creation and management of complex error hierarchies in Rust applications. It allows you to define nested error enums in a straightforward and declarative manner, automatically generating From implementations and other boilerplate code to facilitate error handling across multiple layers of your application.

Features

  • Simplified Error Definitions: Define multiple error enums with nested relationships in a concise syntax.
  • Automatic From Implementations: Automatically generates From implementations for error conversions between different error types, even across multiple layers.
  • Customizable Display Implementations: Use the #[display("...")] attribute to define custom Display messages for your error variants.
  • Custom PartialEq Implementations: Control equality comparisons using the #[cmp_neq] attribute for specific variants.
  • Support for Common Traits: Supports deriving common traits like Clone and PartialEq.

Installation

Add error-tree to your Cargo.toml:

[dependencies]
error-tree = "0.4.0"

Usage

Import the error_tree macro and start defining your error enums using the error_tree! macro:

use error_tree::error_tree;

error_tree! {
    pub enum MyAppError {
        #[display("An unexpected error occurred")]
        UnexpectedError,
        #[display("IO error: {inner}")]
        IoError(std::io::Error),
        #[display("Network error: {url}")]
        NetworkError { url: String },
    }
}

This macro will generate:

  • The MyAppError enum with the specified variants.
  • Implementations of the From trait for converting from wrapped error types to MyAppError.
  • A Display implementation based on the #[display("...")] attributes.

Example: Defining Nested Error Enums

You can define multiple error enums and specify relationships between them. The macro will automatically generate the necessary From implementations for error conversion.

use error_tree::error_tree;
use std::io;

error_tree! {
    pub enum OuterError {
        InnerError(InnerError),
        #[display("An outer error occurred")]
        OuterVariant,
    }

    pub enum InnerError {
        #[display("An inner IO error: {inner}")]
        IoError(io::Error),
        #[display("A custom inner error")]
        CustomError,
    }
}

With this setup, you can convert an io::Error directly into an OuterError:

fn cause_io_error() -> Result<(), io::Error> {
    Err(io::Error::new(io::ErrorKind::Other, "Disk not found"))
}

fn handle_error() -> Result<(), OuterError> {
    cause_io_error()?;
    Ok(())
}

fn main() {
    match handle_error() {
        Ok(_) => println!("Success"),
        Err(e) => println!("Error occurred: {}", e),
    }
}

Automatic From Implementations

The macro generates From implementations that allow seamless conversion between your error types:

impl From<io::Error> for InnerError {
    fn from(inner: io::Error) -> Self {
        InnerError::IoError(inner)
    }
}

impl From<InnerError> for OuterError {
    fn from(inner: InnerError) -> Self {
        OuterError::InnerError(inner)
    }
}

// This allows:
impl From<io::Error> for OuterError {
    fn from(inner: io::Error) -> Self {
        OuterError::InnerError(InnerError::IoError(inner))
    }
}

Customizing Display Messages

Use the #[display("...")] attribute to define custom messages for your error variants:

error_tree! {
    pub enum MyError {
        #[display("Simple error occurred")]
        SimpleError,
        #[display("IO error occurred: {inner}")]
        IoError(std::io::Error),
        #[display("Data error: {data}")]
        DataError { data: String },
    }
}

Controlling PartialEq Behavior

You can derive PartialEq for your error enums and control comparison behavior for specific variants using the #[cmp_neq] attribute:

error_tree! {
    #[derive(PartialEq)]
    pub enum MyError {
        SimpleError,
        #[cmp_neq]
        NonComparableError(std::io::Error),
        DataError { data: String },
    }
}

let error1 = MyError::NonComparableError(io::Error::new(io::ErrorKind::Other, "Error"));
let error2 = MyError::NonComparableError(io::Error::new(io::ErrorKind::Other, "Error"));

assert_ne!(error1, error2); // Due to #[cmp_neq], these are not equal

Advanced Usage

Complex Error Hierarchies

error-tree excels at handling complex error hierarchies. Here's an example adapted from the crate's tests:

use error_tree::error_tree;
use std::sync::mpsc;

#[derive(Debug)]
pub struct CpalStreamError;
#[derive(Debug)]
pub struct CpalDeviceNameError;
#[derive(Debug)]
pub struct CpalDevicesError;
#[derive(Debug)]
pub struct CpalHostUnavailable;

error_tree! {
    pub enum PassiveAudioCaptureError {
        FormatError,
        DeviceError(DeviceError),
        IOError(IOError),
        HostError(HostError),
        StreamError(StreamError),
        ChannelError(ChannelError),
    }

    pub enum DeviceError {
        DeviceNotAvailable { device_name: String },
        Basic(CpalDevicesError),
        NameError(CpalDeviceNameError),
    }

    pub enum IOError {
        Basic(std::io::Error),
    }

    pub enum HostError {
        HostUnavailable(CpalHostUnavailable),
    }

    pub enum StreamError {
        StreamError(CpalStreamError),
    }

    pub enum ChannelError {
        ChannelRecvError(mpsc::RecvError),
    }
}

// Usage example
fn cause_device_error() -> Result<(), CpalDeviceNameError> {
    Err(CpalDeviceNameError)
}

fn main() {
    let result: Result<(), PassiveAudioCaptureError> = (|| {
        cause_device_error()?;
        Ok(())
    })();

    match result {
        Ok(_) => println!("Success"),
        Err(e) => println!("Error: {}", e),
    }
}

In this example:

  • Multiple error enums are defined, representing different components of an audio capture system.
  • The macro generates From implementations, allowing errors from low-level components (like CpalDeviceNameError) to be converted into high-level errors (PassiveAudioCaptureError) automatically.
  • This simplifies error handling in functions that may return errors from different layers.

Tests and Examples

The crate includes several tests demonstrating its capabilities:

  • Clone Derivation Test: Ensures that enums defined with #[derive(Clone)] properly implement the Clone trait.
  • PartialEq Implementation Test: Verifies that custom PartialEq implementations respect the #[cmp_neq] attribute.
  • Display Trait Implementation Test: Checks that custom Display messages are formatted correctly for different variants.

Limitations

  • The macro assumes that the types used in your error variants are valid Rust types that implement necessary traits like Debug and Display where appropriate.
  • For custom types used in wrapped variants, ensure that they implement Debug if you want the default Display implementation to work correctly.

Contributing

Contributions are welcome! Please submit issues or pull requests on the GitHub repository.

License

This project is licensed under the MIT License. See the LICENSE file for details.


Feel free to reach out if you have any questions or need assistance using error-tree.

Appendix: Understanding the Macro's Generated Code

To help you understand what the error_tree! macro generates, here's an overview based on the crate's code:

Error Enums and Variants

The macro processes your error enums and their variants, supporting:

  • Basic Variants: Simple variants without data.
  • Wrapped Variants: Variants that wrap another error type.
  • Struct Variants: Variants with named fields.

From Implementations

For each wrapped variant, the macro generates From implementations to convert from the wrapped type to the enum containing it. It also generates transitive From implementations to allow direct conversion from low-level errors to top-level errors in your hierarchy.

Example generated code:

impl From<std::io::Error> for IOError {
    fn from(x: std::io::Error) -> Self {
        IOError::Basic(x)
    }
}

impl From<IOError> for PassiveAudioCaptureError {
    fn from(x: IOError) -> Self {
        PassiveAudioCaptureError::IOError(x)
    }
}

// Transitive conversion
impl From<std::io::Error> for PassiveAudioCaptureError {
    fn from(x: std::io::Error) -> Self {
        PassiveAudioCaptureError::IOError(IOError::Basic(x))
    }
}

Display Implementations

The macro generates Display implementations for your enums, using the #[display("...")] attribute if provided. If no #[display("...")] attribute is present, it defaults to displaying the variant name.

Custom PartialEq Implementations

If you derive PartialEq and use the #[cmp_neq] attribute on specific variants, the macro generates a custom PartialEq implementation that respects this attribute.

Attribute Handling

The macro carefully processes attributes to ensure that standard attributes like #[derive(...)] are preserved and applied correctly to the generated enums.

Conclusion

error-tree simplifies error handling in Rust applications by reducing boilerplate and providing a clear, declarative way to define complex error hierarchies. By automatically generating necessary implementations, it allows you to focus on your application's logic rather than repetitive error handling code.

Dependencies

~7MB
~110K SLoC