Idiomatic error handling strategy for crates?

Sorry if the title is too broad.

I'm writing a crate exposing a trait, having a method that might return an error.
This error, however, might depend on the concrete implementation, so I'm using an associated type for it.

pub trait Finder {
    type Response;
    type Error;

    fn find(&self, id: u64) -> Result<Self::Response, Self::Error>;
}

However, there is an error every implementations should be able to return.
As an example (using the code from above), it might be a NotFound error.

(NOTE: this is not the case, I'm just simplifying, so no need to suggest using Option here).

My initial idea was to define an error type in the same module, like this:

pub enum FindError<Inner> {
    NotFound,
    Other { cause: Inner },
}

and changing the trait signature as such:

pub trait Finder {
    type Response;
    type Error;

    fn find(&self, id: u64) -> Result<Self::Response, FindError<Self::Error>>;
}

However I think it's a bit too cumbersome, and wanted to get another opinion on this.
How does it look like? Is there a better, more "idiomatic" approach to this use-case?

Thanks <3

One option would be to do this:

struct NotFoundError;

pub trait Finder {
    type Response;
    type Error: From<NotFoundError>;

    fn find(&self, id: u64) -> Result<Self::Response, Self::Error>;
}

See here for an example of this pattern, where you must be able to convert io errors into the error type.

1 Like

That would make the signature cleaner, for sure.

But what if your error is not a NotFound error?
Like a catch-all value for errors that can not be handled from the crate.

I'm thinking something like this:

pub enum FindError {
    NotFound,
    Other { cause: anyhow::Error }, // Or Box<dyn std::error::Error>
}

pub trait Finder {
    type Response;
    type Error: From<FindError>;

    fn find(&self, id: u64) -> Result<Self::Response, Self::Error>;
}

Not sure about the ergonomics of it though...

  1. On the crate side, I'm only interested in handling NotFound error. However, using From would move the actual underlying error, which might be handled by the end-user instead. This would require Clone in the end.
  2. How is it going to play out with ! or std::convert::Infallible, if an implementation doesn't suffer from those errors? The From<FindError> bound might be painful to have...

I'm not quite sure I get what's going on here. What purpose would the Other case have? Why do you need to Clone? It seems like you're using the error in a way I didn't expect.

Let's say this Finder represents an access to an external resource, similar to a database. This operation might fail for a number of different reasons, e.g. network error.

Other failures, however, have a clear meaning, like the NotFound variant, and should be able to be handled by the crate internal component.

So, how could I distinguish between these two kind of errors, using the pattern you're describing?

From doesn't seem to do the job, because From semantics implies an infallible conversion.
Maybe TryFrom?

pub enum FindError {
    NotFound,
    SomeOtherCoolError,
}

pub trait Finder {
    type Response;
    type Error: TryFrom<FindError>; // Shouldn't it be TryInto instead?

    fn find(&self, id: u64) -> Result<Self::Response, Self::Error>;
}

Using TryFrom doesn't really make sense. That would mean that creating the error objects themselves could somehow fail. Are you perhaps instead looking for a way to check if Finder returned an NotFound after it has been returned from find?

Correct.
Let's say there's another component in the crate that uses this Finder.

The find method returns an error, which might either be a "not found" error, or another error (e.g. network error).

This "another error", however, might change depending on the implementation: an "in-memory" implementation for example doesn't suffer from network errors.

The NotFound error would be handled by this other component in the crate, or might also be handled by end-users.

I don't know if it makes sense...

How about this then?

trait FinderError: std::error::Error {
    fn is_not_found(&self) -> bool;
}

pub trait Finder {
    type Response;
    type Error: FinderError;

    fn find(&self, id: u64) -> Result<Self::Response, Self::Error>;
}

Damn, why haven't I think of that!
I'll try that one out, thanks!

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.