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).
WireframeError<E>is a public library error type but derives onlyDebug. WithoutDisplayandstd::error::Error, it cannot participate in the standard error ecosystem: it cannot be wrapped byanyhow, returned throughBox<dyn Error>, used withthiserror's#[from]or#[source], or inspected viastd::error::Error::source.The blocking constraint is the
Protocol(E)variant.std::error::ErrorrequiresDisplay, andDisplayrequiresE: Display. The defaultE = ()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:
2. Specific impl for the
E = ()default (unlocks#[from]inTestError):These two impls are not overlapping because
()does not implementstd::error::Error.Downstream effect on
TestErrorOnce
WireframeError<()>implementsstd::error::Error, the manualFromimpl insrc/testkit/result.rscan be replaced with#[from]:and the
impl From<WireframeError> for TestErrorblock deleted entirely.Suggested path
This change is small, additive, and non-breaking (adding new trait impls cannot break existing callers).