More ergonomic wrapper for repeated map_err() with custom message

I have an enum called TmrConnectError set up using thiserror:

#[derive(Debug, thiserror::Error)]
pub enum TmrConnectError {
    #[error("Authentication failed: {msg}")]
    AuthError {
        msg: String,
        source: Option<Box<dyn std::error::Error + Send + Sync>>,
    },

I often map_err rmcp::transport::AuthError into this error (14 places), but I also have some places without that error as the source.

I wanted to do the creating of the error more ergonomic, so I first thought about #[from] and impl From<rmcp::transport::AuthError> for TmrConnectError, but I realized that #[from] does not support leaving out the rmcp error and that I can not provide my own informative message.

So instead, I have created a trait to hook a function function onto the rmcp error.

Does this look like a nice solution to you? Are there nicer alternatives?

Original call-site:

oauth_state
    .handle_callback(&auth_code, &csrf_token)
    .await
    .map_err(|e| TmrConnectError::AuthError {
        msg: "Failed to handle authorization callback".to_string(),
        source: Some(e.into()),
    })?;

Trait:

pub(crate) trait MapAuthToConnectError<T> {
    fn to_connect_err(self, msg: impl Into<String>) -> Result<T, TmrConnectError>;
}

impl<T> MapAuthToConnectError<T> for Result<T, rmcp::transport::AuthError> {
    fn to_connect_err(self, msg: impl Into<String>) -> Result<T, TmrConnectError> {
        self.map_err(|e| TmrConnectError::AuthError {
            msg: msg.into(),
            source: Some(Box::new(e)),
        })
    }
}

Updated call-site:

oauth_state
    .handle_callback(&auth_code, &csrf_token)
    .await
    .to_connect_err("Failed to handle authorization callback")?;

I just realized that I could also do a free-standing function, but I think it will look more messy at the call-site:

fn to_connect_err(auth_err: rmcp::transport::AuthError, msg: impl Into<String>) -> Result<T, TmrConnectError>;

edit: I just realized that I might want to append the inner error message to the outer one. Should be possible to do in my function.

yes, this is a good solution.

this is essentially a customized version of ResultExt::context() in snafu.

alternatively, your example is almost identical to snafu::Whatever, so you can rewrite it using snafu:

#[derive(Debug, snafu::Snafu)]
pub enum TmrConnectError {
	#[snafu(whatever, display("Authentication failed: {message}"))]
	AuthError {
		message: String,
		#[snafu(source(from(Box<dyn std::error::Error + Send +Sync>, Some)))]
		source: Option<Box<dyn std::error::Error + Send + Sync>>,
	},
}

oauth_state
    .handle_callback(&auth_code, &csrf_token)
    .await
    .whatever_context("Failed to handle authorization callback")?;

Nice, I didn't know about snafu!

It looks like thiserror but with some anyhow-like features?

snafu helps derive Display and Error implementations, similar to thiserror. though the syntax for the derive macro is slightly different.

it creates "context selector" types, so you can easily map the source error into your custom error with the result.context(...) api. the main difference is, snafu error types and contexts are statically typed, while anyhow uses type erasure.

another similarity to anyhow is "stringly-typed" errors for quick and convenient error reporting, i.e. the Whatever type in snafu.[1]

so yeah, your summary is more or less correct. snafu is designed to be suitable for both libraries and applications, kind of like the combination of thiserror and anyhow, but in its own way.

you can read the author's own words in the user guide, especially the design philosophy section, of the documentation:


ps, I also recommend this blog post on the topic of error handling:


  1. in fact, you can combine snafu and anyow. I have successfully used anyhow::Error as the source field of my error derived using snafu, but the documentation of snafu::Whatever only shows Box<dyn std::error::Error> ↩︎