Custom error guidelines?

#1

Has anyone written up common guidelines for custom errors in Rust? For example, how do people generally organise errors? Into their own module? Should errors aim to be specific or generic with some value to disambiguate? There seems to be quite a bit of boilerplate required even for error enums which makes me think very general error types are preferred?

1 Like

#2

The only guidelines I know of

For example, how do people generally organize errors? Into their own module?

I agree we should provide some guidance on this. For me, it depends on the size of the crate and the number of error stuff. If the crate is big enough, I have errors hidden away in a mod. I think I’m inconsistent on calling it error or errors.

Should errors aim to be specific or generic with some value to disambiguate?

I have two general guidelines on this

  • What kind of programmatic decisions might people make on this error?
  • Are my errors independent of implementation details?

I use a enum Kind for programmatic decisions and a String or Box<Display> for additional context.

4 Likes

#3

Thanks for the advice. The failure crate looks to be particularly helpful for avoiding a lot of the boilerplate.

0 Likes

#4

Thanks for the advice. The failure crate looks to be particularly helpful for avoiding a lot of the boilerplate.

To be clear, I was pointing to the principles and not the implementation. If you are creating a CLI, its fine. The API is not stable and errors are pretty fundamental to your API, so I’d recommend against it for libraries.

0 Likes

#5

I recommend this if you want to use failure.

0 Likes

#6

It seems there are two main approaches in Rust:

  1. The enum approach, either implemented manually or with help of error-chain or quick-error, where every error is an enum of the ways a function can fail. Errors can be combined into bigger enums of enums.

  2. The failure crate, which aims to have one general error type, and encourages exploring chain of errors and downcasting to specific types to learn more about error details if you need to.

My current experience is that approach #1 is best for small libraries. quick-error is the minimum amount of boilerplate and you get error type that is very clear, easy to use and (if you keep the enum small) very efficient (no heap allocations).

The second approach is convenient in applications, especially as they grow large and complex enough that it’s too much to handle each individual error reason. You can then slap failure::Error in every single return type and worry about it later.

As for organization of code: in Rust it’s common to put every functionality in a separate module (often a private module) and then re-export the most important bits at the top of the library. So you’d have mod error; pub use error::*, so that users of your library can use yourlibrary::Error.

3 Likes

#7

Thanks all. I’m currently investigating using errors without libraries until I properly understand how the different systems work.

One thing I found initially appealing for my needs was defining an enum for each function that lists exactly the errors that are expected to be handled. This allows Rust to warn about exhaustive matching when using match on returns. So for example:

// my_function_name errors are: Error1 and Error2
match my_function_name() {
    Ok(value) => {}
    Err(MyFunctionNameError::Error1(error_value)) => {}
    Err(MyFunctionNameError::Error2(error_value)) => {}
}
// my_other_function_name errors are: Error1, Error2 and Error3
match my_other_function_name() {
    Ok(value) => {}
    Err(MyOtherFunctionNameError::Error1(error_value)) => {}
    Err(MyOtherFunctionNameError::Error2(error_value)) => {}
    Err(MyOtherFunctionNameError::Error3(error_value)) => {}
}

However it would be nicer to be able to use global errors for matching instead of function specific ones, and yet still keep the exhaustive matching. Something like:

match my_function_name() {
    Ok(value) => {}
    Err(LibError::Error1(error_value)) => {}
    Err(LibError::Error2(error_value)) => {}
}
match my_other_function_name() {
    Ok(value) => {}
    Err(LibError::Error2(error_value)) => {}
    Err(LibError::Error3(error_value)) => {}
    Err(LibError::Error4(error_value)) => {}
}

I know I can use the from trait to convert between these but I can’t figure out a way to make use of that within the pattern syntax.

Or maybe I’m barking up completely the wrong tree.

0 Likes

#8

You can use one enum per function (or rather per related group of failures), and provide From implementations to convert between all of them. quick-error automates generation of that code.

The ? operator automatically calls From on all errors, so having lots of different error types isn’t a problem as long as they are convertible.

1 Like

#9

Right but my point was that as far as I can tell you can’t directly match a local enum variant to a global one. So the library user either has to juggle lots of different enum types or else casts to the more general error type and thus loses match exhaustion over the local one.

0 Likes

#10

You can match into multiple levels of enums:

match err {
   WrapperType::Other(WrappedType::Specific(..)) => {}
}

The exception is io::Error which annoyingly has a .kind() as a method, so you have to do that in two steps.

0 Likes

#11

I don’t like this characterization, really. failure advocates a variety of different types of error APIs, depending on what you’re writing.

Here’s what the book says about using Error:

When might you use this pattern?

This pattern is very effective when you know you will usually not need to destructure the error this function returns. For example:

  • When prototyping.
  • When you know you are going to log this error, or display it to the user, either all of the time or nearly all of the time.
  • When it would be impractical for this API to report more custom context for the error (e.g. because it is a trait that doesn’t want to add a new Error associated type).

I really do not believe that users are meant to downcast Errors they get from libraries; this is dangerous as the downcast can silently fail when linking multiple versions of a crate. Rather, libraries shouldn’t be returning Error in the first place, and an application can downcast things it has converted into Error as a last resort.

For libraries, failure advocates:

  • small, special-purpose errors where they matter, for utilities and algorithms. Picture the gold standard set forth by types like std::str::Utf8Error, which has useful methods like valid_up_to. failure's automatic derived traits make these somewhat easier to write.

  • Error and ErrorKind pairs not unlike those generated by error-chain, for mid-level library code that may fail in a variety of ways, and which may need to be extended. For instance, deserialization may involve anything from syntax errors (e.g. reading text to JSON) to schema errors (e.g. an array where a string was expected) to general IO errors.
    Unlike error-chain, these must be hand-crafted. They can get boilerplatey, and there’s not too much you can do about it.

Ultimately, the general theme I feel is that failure wants libraries to have well-designed errors with real thought and energy put into them, and it strives to make these (just a bit!) easier.


In contrast, I view the error-chain philosophy as making errors as low resistance as possible, so that people prefer errors over panics. To this end, it provides a one-size-fits-all solution for use by libraries (be they low or mid-level) and applications alike.

And to be honest? I’ve never been a fan of that. Left unchecked, natural use of error-chain can lead to big, underspecified, sloppy “mega-error” types.

4 Likes

#12

I tend to think that specific errors are better than generic ones. They allow fine-grained error management, easier debugging, cleaner code and better error recovery.
However, as you mentioned, there is a lot of boilerplate involved in the definition of error types. That’s why I created the following crate, that makes defining custom errors a breeze:

custom_error

This crate contains the custom_error! macro, that creates error enums and implements the Error trait automatically for them. It allows you to write error types as simply as:

custom_error!{
  /// An error generated when reading a number from a file fails 
  FileParseError
    Io{source: io::Error}         = "unable to read from the file",
    Format{source: ParseIntError} = "the file does not contain a valid integer",
    TooLarge{value:u8}            = "the number in the file ({value}) is too large"
}

With this crate, it becomes easy to generate many different fine-grained error types, that can be converted between each other easily with the ? operator. You can now easily return a different error type for each of your functions.

1 Like