Skip to content

WireframeError<E> should derive Display and std::error::Error #513

Description

@leynos

WireframeError<E> is a public library error type but derives only Debug. Without Display and std::error::Error, it cannot participate in the standard error ecosystem: it cannot be wrapped by anyhow, returned through Box<dyn Error>, used with thiserror's #[from] or #[source], or inspected via std::error::Error::source.

The blocking constraint is the Protocol(E) variant. std::error::Error requires Display, and Display requires E: Display. The default E = () satisfies neither, so a single blanket impl cannot cover the full type.


Recommended approach: two complementary impls

1. Blanket impl covering all real protocol-error types:

use std::fmt;

impl<E: fmt::Display + fmt::Debug> fmt::Display for WireframeError<E> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::DuplicateRoute(id) => write!(f, "duplicate route: {id}"),
            Self::Io(e)             => write!(f, "I/O error: {e}"),
            Self::Protocol(e)       => write!(f, "protocol error: {e}"),
            Self::Codec(e)          => write!(f, "codec error: {e}"),
        }
    }
}

impl<E: std::error::Error + 'static> std::error::Error for WireframeError<E> {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Self::Io(e)    => Some(e),
            Self::Codec(e) => Some(e),
            Self::Protocol(e) => Some(e),
            Self::DuplicateRoute(_) => None,
        }
    }
}

2. Specific impl for the E = () default (unlocks #[from] in TestError):

impl fmt::Display for WireframeError<()> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::DuplicateRoute(id) => write!(f, "duplicate route: {id}"),
            Self::Io(e)             => write!(f, "I/O error: {e}"),
            Self::Protocol(())      => write!(f, "protocol error"),
            Self::Codec(e)          => write!(f, "codec error: {e}"),
        }
    }
}

impl std::error::Error for WireframeError<()> {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Self::Io(e)    => Some(e),
            Self::Codec(e) => Some(e),
            _              => None,
        }
    }
}

These two impls are not overlapping because () does not implement std::error::Error.


Downstream effect on TestError

Once WireframeError<()> implements std::error::Error, the manual From impl in src/testkit/result.rs can be replaced with #[from]:

#[error(transparent)]
Wireframe(#[from] WireframeError),

and the impl From<WireframeError> for TestError block deleted entirely.


Suggested path

This change is small, additive, and non-breaking (adding new trait impls cannot break existing callers).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions