5 ms·
Designing Error Types in Rust Libraries
- vlovich123 1y agoThis post overlooks a very important technique to get type hiding without the cost of boxing. Having an opaque public type that wraps the private one: public struct SqlError(sqlx::Error) #[derive(Debug, thiserror::Error)] pub enum MyError { #[error("Database error: {0}")] DatabaseError(#[from] SqlError) } It’s a bit more annoying to construct since result has to have a .map_error(SqlError) but that’s not that much honestly.
- saghm 1y agoThis also gives the flexibility to easily change your decision later if you determine that it's important to expose the inner type for some reason. With a boxed trait error type, you'd only be able to by adding a new method to one of the traits, and the implementation would be a lot messier with downcasting, (and unwrapping if you wanted to avoid the optional, which could open the door to bugs where you accidentally don't have the error type you expect and then panic).
- 5422m4n 1y agoThat's a valid point, it never occurred to me that this pattern remains useful. But I certainly see the charm it has besides being a bit more annoying to handwrite. I will update the post. Thanks
- saghm 1y agoIn addition to the tradeoffs the article discusses, one other potential use for using traits rather than concrete error types is that you could also choose to use a different container than `Box` if you need different properties. As an example, one library I worked on needed to track errors that might occur in background threads and sometimes return them (or merge the info in them with other errors) to users when an operation failed later. Because the client the library provided could itself be used concurrently, this meant that we sometimes had to return the same error (or at least, parts of the same information from the error) in multiple different threads, and not all of the error types we wrapped were possible to Clone. Wrapping these errors internally in an `Arc` made it possible for us to clone our top-level error type and greatly simplify our internal logic handling these cases. I imagine that this isn't a particularly common problem people well run into, but at the very least being able to wrap `dyn Error + Send + Sync` is a nice example of how composable a lot of the concepts in how to design an API in Rust are.
- horacimacias 1y agonice article, thanks for sharing. Regarding Solution 2, you can also use thiserror on the "custom built wrapper types", which eliminates the need to manually create the From implementations. use thiserror::Error; #[derive(Error, Debug)] #[error(transparent)] pub struct DbError(sqlx::Error); #[derive(Error, Debug)] #[error(transparent)] pub struct HttpError(reqwest::Error); #[derive(Error, Debug)] pub enum MyPublicError { #[error("database error: {0}")] DbError(#[from] DbError), #[error("http error: {0}")] HttpError(#[from] HttpError), } fn main() -> Result<(), MyPublicError> { do_db_stuff()?; Ok(()) } fn do_db_stuff() -> Result<(), DbError> { todo!() }