My practice designing an error type

⚓ Rust    📅 2025-09-29    👤 surdeus    👁️ 11      

surdeus

Warning

This post was published 113 days ago. The information described in this article may have changed.

After a few months of learning Rust, I’ve developed my own approach to designing error types. I’m not sure if it’s entirely sound or if it could use improvement, so I’m writing it down here for feedback. PS: this post is polished by GPT.

Core Idea

I mainly use thiserror to define error types. For example:

#[derive(Debug, Error)]
pub enum MyError {
    #[error("Io error")]
    Io(
        #[from]
        #[source]
        IoError,
    ),

    #[error("Http request error")]
    Request(
        #[from]
        #[source]
        RequestError,
    ),
}

My design principle is:

  • Errors that must be handled at a higher level get their own dedicated enum variant.
  • Errors that don’t need higher-level handling are grouped into a general “catch-all” variant, mainly for debugging purposes (e.g., printing the error chain at the top level).

For example, I use anyhow to wrap those unhandled errors:

use anyhow::Error as AnyError;

#[derive(Debug, Error)]
pub enum MyError {
    #[error("Io error")]
    Io(
        #[from]
        #[source]
        IoError,
    ),

    #[error("Http request error")]
    Request(
        #[from]
        #[source]
        RequestError,
    ),

    // Catch-all for errors not explicitly handled
    #[error(transparent)]
    Any(
        #[from]
        AnyError
    )
}

This way, I only destructure and handle the important variants, while still being able to print a full error chain when needed.

Simplification for Small Apps

When writing a small application where all underlying errors can be enumerated, I prefer to implement my own AnyError. This allows me to provide lightweight contextual information without relying on anyhow.

Here’s an example:

/// Error wrapper with contextual message
#[derive(Error, Debug)]
#[error("{msg}")]
pub struct ContextedError<E: StdError> {
    msg: Cow<'static, str>,
    #[source]
    source: E,
}

/// Create a [`ContextedError`]
pub fn create_contexted_error<E: StdError>(
    msg: impl Into<Cow<'static, str>>,
    e: E,
) -> ContextedError<E> {
    ContextedError {
        msg: msg.into(),
        source: e,
    }
}

I also add helper traits so that Result can easily attach context to errors:

/// Convert an error into a [`ContextedError`]
pub trait IntoContextedError<T = Self>: StdError + Into<T>
where
    T: StdError,
{
    fn into_contexted_error(self, msg: impl Into<Cow<'static, str>>) -> ContextedError<T> {
        create_contexted_error(msg, self.into())
    }
}

/// Add context to a `Result` error
pub trait ContextedResult<V, S: IntoContextedError> {
    fn add_context(self, msg: impl Into<Cow<'static, str>>) -> Result<V, ContextedError<S>>;
    fn with_context<F: Fn() -> Cow<'static, str>>(self, f: F) -> Result<V, ContextedError<S>>;
}

/// Turn result error into target error, then add context
pub trait TargetContextedResult<V, E: IntoContextedError<T>, T: StdError> {
    fn convert_then_add_context(
        self,
        msg: impl Into<Cow<'static, str>>,
    ) -> Result<V, ContextedError<T>>;

    fn convert_then_with_context<F: Fn() -> Cow<'static, str>>(
        self,
        f: F,
    ) -> Result<V, ContextedError<T>>;
}
impl<V, E: IntoContextedError<T>, T: StdError> TargetContextedResult<V, E, T> for Result<V, E> {
    fn convert_then_add_context(
        self,
        msg: impl Into<Cow<'static, str>>,
    ) -> Result<V, ContextedError<T>> {
        self.map_err(|e| e.into_contexted_error(msg))
    }
    fn convert_then_with_context<F: Fn() -> Cow<'static, str>>(
        self,
        f: F,
    ) -> Result<V, ContextedError<T>> {
        let msg = f();
        self.convert_then_add_context(msg)
    }
}

Finally, I can define a general AnyError type like this:

// General-purpose error type
type AnyError = ContextedError<AnyErrorKind>;

#[derive(Error, Debug)]
enum AnyErrorKind {
    // Collects all errors not worth handling individually
}
impl<E: Into<AnyErrorKind> + StdError> IntoContextedError<AnyErrorKind> for E {}

Summary

  • Application errors: Defined with thiserror, separating important (handled) errors from unimportant ones.
  • Unhandled errors: Collected into a general Any variant (either with anyhow or a custom AnyError).
  • Small applications: Prefer a custom AnyError for lightweight control over error context.

This design ensures that important errors are explicitly handled, while all errors—handled or not—retain full debug context through error chains.

1 post - 1 participant

Read full topic

🏷️ Rust_feed