First, apologies for the long post and any incoherencies in it. I've been meaning to start a discussion about errors for a while already so there has been some build-up of thoughts and text fragments written over many weeks. There are several explicit questions in the text below, but please do share your thoughts and comments in general on the topic too.
The Rust book has a nice chapter about error handling, but it concentrates on the more technical side of it. I'm also interested in the "softer" and more semantic side of error handling and that's the theme of this post. There also seems to be some value for having at least some parts defined more precisely overall for things to work smoothly. An example of that coming up right in the first subsection below.
The Error
trait
The std::error::Error
trait has three major facets:
fn description()
trait Display
fn cause()
The cause
function seems to be the most obvious one, when considering what it should do. When to use it seems more trickier, but maybe the section about errors from other libraries below covers that, so moving on to the description()
and Display
.
The function description()
returns a &str
, and to me there seems to be an implication it probably returns a message from a set of 'static short basic descriptions. The alternative would be for the error type to own a custom formatted String for every error instance, and that doesn't sound like its usually desirable.
However, when thinking about what the Display
implementation should do, I got stumped on a really elementary question: should it contain the text from description()
or not? If there's no common rule to follow, we will end up outputting messages like "Basic description: Basic description: A more detailed description" or just "A more detailed description" without any common text to give context to the details. Both approaches have their merits. "Give more power to the user over formatting the message" and "make the life easier for the user and give fully fleshed out messages out of the box."
It would be very nice if there was a more specific common guideline for the above part, so that everyone could trust things to work similarly. Or at least to have the policy of explicitly telling the chosen approach in documentation and being consistent with it within a single crate!
Level of detail
How much information should an error structure contain? At one extreme it contains everything that has any relevance to the situation at all, and I'm somewhat worried that the error types can easily grow to be unreasonably large. Maybe I'm just overguesstimating the cost of having relatively large values on stack, but it also seems kind of disproprtionate to have something like Result<u8, SomeReallyLargeErrorType>
. Anyway, now that I brought memory usage up, I also feel like allocating memory to construct an error struct is mostly not a good idea, but am not sure if there's all that much reason for that.
At the other end of the continuum the error type is maybe just an enum with no other info on the error except for telling very generally what kind of error happened. With this approach the usefulness of the error type goes down. It wouldn't be very fun to code in Rust if the compiler just printed out "lifetime error" with no other information.
Errors from other libraries
Let's say you're making a library, but you're not doing everything by yourself, you are using other libraries to do things for you. Do you think it's okay to pass through errors, therefore exposing implementation details of your library to the user? Think about an error enum whose variants are actually error types from those other libraries. There's a choice to either make the contained values within the variants public or not.
I'm personally on the fence with this. Hiding the underlying libraries let's me keep the API under my control, but on the other hand, if more detailed error types are the way to go, I would probably practically end up repeating the error type definitions from the other library at least partially.
At some point I remembered the Error trait has the cause() function. Maybe the error can be given to the caller as &Error, therefore not actually exposing the specific error type, while still giving them access to it. Of course accessing any fields of the original error is not possible this way. This also seems to interact with how my own error type should implement description()
and Display
- when should I just grab them from the original error and when should I add my own messages? Should I rely on the caller traversing the cause hierarchy to display as stack of error messages?
Finally, there are the error types from the standard library. Is it a good idea to use std::io::Error
for I/O-related errors even when the function is not under std::
but it otherwise matches the use case?
Error enumerations
It looks like in many cases the error types are best represented by an enum, with a variant for each major kind of error that can happen. Now, here at least two options seem to come up in a way similar to what I brought up above: there's a possibility for a more or less detailed error handling here too. Let's assume two functions, foo and bar, with the possible error cases for foo being either A
or B
, and for bar the potentially returned errors are B
and C
.
Should there be two error enums, {A,B}
and {B,C}
or just one, {A,B,C}
?
fn foo() -> Result<SomeType, AB>
fn bar() -> Result<OtherType, BC>
vs.
fn foo() -> Result<SomeType, ABC>
fn bar() -> Result<OtherType, ABC>
The first option is more precise, the caller should have exact knowledge of what errors may happen. Then again, I'm not really sure if it is all that usable for the caller (think about having to write completely separate matches for all Result
-returning functions, with no chance for a general error handler of library X's errors), never mind being more work for the library author too.
Target audiences
When writing error messages, we should remember the messages won't necessarily be seen only by other programmers, but the end users with less technical knowledge or understanding of the context of the error. Here I think the programmer of the actual user-facing program has a special responsibility to make the error messages good for users, but library authors have some responsibility too. If not anything else, at least I feel it's not a good idea to have messages like "I HATE YOU"/"I LOVE YOU" message when authenticating with a CVS server. Those messages may and will bubble up to the GUI level and you end up having your program showcased on The Daily WTF.