Error design guidelines #966
Replies: 2 comments 1 reply
-
I largely agree with what you've said. I'll also note that
I don't quite agree with this. From a library perspective I'm sure its nice not having to worry about semver breakages, but I almost pedantically never wild-card error variants in matches. I want to be forced to think about new error variants. If I'm matching I clearly had some subset that I cared about - who's to say this new variant doesn't fall into that category? One can be carefully aware of this and ensure one checks all references to the error type. This sort of works as the PR author; but does mean reviewers cannot trivially check for correctness without being aware of all instances themselves.
This does make
I prefer the richness of
I think this is a good idea. It also enhances loop based tests by making it trivial to embed the iteration info with for i in x..y {
foo(inputs[i]).with_context(|| format!("Iteration {i}"))?;
} I'll also add my 2c that we overuse error variants. At least from the node perspective we could replace 99% instances with |
Beta Was this translation helpful? Give feedback.
-
Agreed!
I don't have a strong preference here. Maybe for now we keep things as they are (i.e., keep enums exhaustive) and create a separate issue to make error variants non exhaustive in the future.
Agreed!
I prefer the source approach.
I like this - but I'd probably do this as follow-up work. |
Beta Was this translation helpful? Give feedback.
-
Unless I missed it, our error handling story hasn't been fleshed out yet and it seems we were waiting on
thiserror
to become no-std compatible, which it now is. This means we can rework our errors now but before we do that, I think we could throw some considerations into the mix.The following suggestions are roughly ordered from presumably least controversial to somewhat controversial. Happy to receive any input!
Use
core::error::Error
With
core::error::Error
stabilized since Rust 1.81 we can now rewrite the feature-gated impls:This is only relevant when not using
thiserror
.Use
thiserror
Additionally,
thiserror
2.0.0 has been released and supportsno_std
environment. In a discussion inmiden-vm
there seems to have been consensus on usingthiserror
.Lowercase, no-punctuation error message
For best interoperability we should follow this advice from the Rust API Guidelines:
This is good and necessary because we can't anticipate if error messages will be embedded in others (typically separated by
:
) and capitalizing or adding punctuation makes the error message look odd.Update failed.: Value was wrong.: Amount exceeded.
update failed: value was wrong: amount exceeded
Make error enums
non_exhaustive
The rational is that users can still match on specific errors, but since errors are rarely handled exhaustively making an error enum exhaustive is frequently unnecessary. On the win-side it does make the error future-proof as we can add new variants in a non-breaking way (e.g. particularly important post-
1.0
, but even now between0.X
versions).Don't implement Clone and PartialEq
We should avoid implementing
Clone
,PartialEq
andEq
for our errors (as we currently do inmiden-base
) as thecore::error::Error
trait does not requireClone
orPartialEq
and thus errors do not generally implement those. Implementing those traits means we require it on all errors we wrap too and this makes wrapping errors that do not implement these traits impossible. Examples where we have this problem are here:miden-base/objects/src/errors.rs
Lines 24 to 26 in a628652
It also means we cannot add a
OpaqueVariant(Box<dyn core::error::Error + Send + Sync>)
variant to an enum because that boxed error does not implementClone
andPartialEq
.Locally I removed the mentioned derive macros from our errors everywhere to see where it would break and it seems we were only using it (at least in
miden-base
) to compare errors in tests. Here we can replaceassert_eq!(error, MyError::Err)
withassert!(matches!(error, MyError::Err))
(orassert_matches!
). I did not find a compilation error for the missingClone
.Display XOR source
When implementing errors one has to make a choice between two possible styles. There is guidance in Rust which says that:
This can be summarized as "Display XOR source". So, using
thiserror
exactly one of the following:or
Despite that repo being archived, the advice is still valid. Error reporters like
anyhow
oreyre
(perhaps alsomiette
?) iterate the source errors and produce nice reports. If one includes the error in the message and returns it fromsource
then it will cause duplicate error messages. The guidance just says that we should not do both{0}
and#[source]
which is easy enough and avoids the duplication, but deciding which one to actually use is not straightforward.The
source
style gives error reporters the chance to format errors in different ways (single-line vs. multi-line, color vs. no-color, etc.), e.g. usinganyhow
with the source-style example above:On the other hand, with the include-in-Display style we get a slightly less nice report, although the difference here is just in readability:
The potentially biggest issue with source style is that that users who are not aware of reporters and simply print an
AccountError
would just seeasset vault failed to update
because that's all theDisplay
impl of that type does and expects a reporter to do the rest. There hasn't been a fix on thestd
/core
level for this yet (see also the discussion in the linked thread above). So we have to decide what route we want to take, the nice one but with the arguably worse UX (source-style) or the less nice one but with hard-to-get-wrong UX (include-in-Display).When using include-in-Display style and we wrap an error type that does follow the source-style approach, we need to build a report ourselves in our Display impl, otherwise we end up swallowing some errors.
Another thing to note is that, to the best of my knowledge, developers typically unwrap on errors in prototype or test code (where errors are particularly prevalent), and if an error occurs they see the result of
Debug
and notDisplay
. They have to go out of their way (unfortunately) to see the much better error message. Hence using the source-style would only be an issue if developers were displaying errors themselves without using something likeanyhow
. Since most devs do use a convenient crate like that I think source-style wouldn't be as big of a problem for the developer experience.My personal opinion is that while there is no guarantee that error reporters are being used they often are when actually printing error messages instead of unwrapping them (which uses the
Debug
impl). It seems to me that the Rust ecosystem prefers the source-style for its higher flexibility. Both styles have their caveats (source style needing reporters and include-in-Display style needing reporters when wrapping source-style errors), so I would lightly suggest to just go with source style.Use Result in tests
The above discussion spawned another thought for me: Since we always unwrap in test code and thus see the
Debug
impl of errors, couldn't we make life for ourselves better by writing our tests with aResult<(), anyhow::Error>
type and use?
instead ofunwrap
in test code?Together withRUST_BACKTRACE=1
we would get our own nice error messages if tests fail (plus validating that they are in fact nice or prompting an improvement) instead of the debug representation. With the backtrace (whichanyhow
adds with that env var) we could still easily figure out which line in the test code failed, so we wouldn't loose that information compared to the panic caused by unwrap.This doesn't suggest to rewrite all our tests using this style immediately. We we could just start experimenting with this.
Summary
To summarize the suggestions in this issue:
impl core::error::Error for MyError {}
unless usingthiserror
.thiserror
whenever possible to derive theError
trait impl.non_exhaustive
.Clone
andPartialEq
) for Errors that source errors do not implement so we can properly wrap them.Result<(), anyhow::Error>
in tests and whether it helps with debugging tests.Beta Was this translation helpful? Give feedback.
All reactions