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.
Uh oh!
There was an error while loading. Please reload this page.
-
Unless I missed it, our error handling story hasn't been fleshed out yet and it seems we were waiting on
thiserrorto 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::ErrorWith
core::error::Errorstabilized since Rust 1.81 we can now rewrite the feature-gated impls:This is only relevant when not using
thiserror.Use
thiserrorAdditionally,
thiserror2.0.0 has been released and supportsno_stdenvironment. In a discussion inmiden-vmthere 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 exceededMake error enums
non_exhaustiveThe 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.Xversions).Don't implement Clone and PartialEq
We should avoid implementing
Clone,PartialEqandEqfor our errors (as we currently do inmiden-base) as thecore::error::Errortrait does not requireCloneorPartialEqand 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:https://github.com/0xPolygonMiden/miden-base/blob/a62865204823e078037fbb31c0f78ef402e3d654/objects/src/errors.rs#L24-L26
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 implementCloneandPartialEq.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
thiserrorexactly one of the following:or
Despite that repo being archived, the advice is still valid. Error reporters like
anyhoworeyre(perhaps alsomiette?) iterate the source errors and produce nice reports. If one includes the error in the message and returns it fromsourcethen 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
sourcestyle gives error reporters the chance to format errors in different ways (single-line vs. multi-line, color vs. no-color, etc.), e.g. usinganyhowwith 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
AccountErrorwould just seeasset vault failed to updatebecause that's all theDisplayimpl of that type does and expects a reporter to do the rest. There hasn't been a fix on thestd/corelevel 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
Debugand 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
Debugimpl). 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
Debugimpl of errors, couldn't we make life for ourselves better by writing our tests with aResult<(), anyhow::Error>type and use?instead ofunwrapin test code?Together withRUST_BACKTRACE=1we 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 (whichanyhowadds 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.thiserrorwhenever possible to derive theErrortrait impl.non_exhaustive.CloneandPartialEq) 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