As some of you may know, I am looking to leverage Rust's powerful error-handling capabilities with a minimum of boilerplate code. I am developing what will become a large project and want to make early investments which help keep the code correct, maintainable and efficient.
I've come across a problem with Error
s and I wonder 1) if I've missed something significant, and if not, 2) whether there is sufficient interest in exploring a solution to this problem or 3) whether I should simply move forward on my own, building for my needs.
If there is sufficient interest, I'm willing to either join an existing effort or begin a new one to resolve this; while I have some flexibility, reasonable timeliness to a solution is an important component of the solution to me. Please let me know by offering your suggestions/fixes/ideas.
Ok, so what's the problem?
No consistent basis with which to work with Errors
(inconsistencies or issues in bold)
using the Standard Library
-
e.g.
std::fmt::Error
:- is a (marker) struct
- does implement PartialEq (among other traits)
- does not implement a kind() method
- does not define ErrorKind variants
-
e.g.
std::io::Error
:- is a struct encapsulating an enum
- does not implement PartialEq
- does implement a kind() method
- does implements ErrorKind variants which implement PartialEq
-
std::error::Error
- is a trait
- does not define or implement PartialEq
- does not define or implement a kind() method
- does hot define ErrorKind variants
using error-chain
-
error-chain
- is Rust's defacto-standard error-handling crate
- does not implement PartialEq
- does implement a kind() method
- does implement ErrorKind variants which do not implement PartialEq
using pattern matching
- pattern-matching (as opposed to via PartialEq) is often cited as the recommended way to distinguish Errors, given these numerous inconsistencies). There are crates (such as matches) for reducing boilerplate when pattern-matching
- unfortunately, pattern matching
Error
s adds boilerplate, obfuscates code and is an incomplete solution. e.g.:
fn string_validator_ref_handles_empty_input() {
// GivenNotes:
let input = String::new();
let expected_result = ErrorKind::ValueNone;
// When some operation which generates an error occurs
let result = (&input).validate_ref::<NonEmptyStringValidator>().err().unwrap()
/* Idealized */
// Then ensure the expected error was returned
assert_eq!(*result, expected_result);
}
/* Reality */
// Then ensure the expected error was returned
assert!(matches!(*result, expected_result); //Error: pattern unreachable (match doesn't work this way; unintuitive at this level)
assert!(matches!(*result, val if val == expected_result)); //not ergonomic; Error without PartialEq (e.g. error-chain): binary operation `==` cannot be applied to type `error::ErrorKind`
assert!(matches!(*result, ErrorKind::ValueNone); //works, but without variable support, important scenarios become unrepresentable:
// suppose two methods must yield same result. To ensure consistency through refactorings, a test is developed:
// assert_eq!(matches(FuncA(badInput), FuncB(badInput)) //not possible, as per above; only constant variants w/PartialEq implemented can be used ergonomically
When considering Error equality, I believe it is out of scope for to consider the semantics of errors (e.g. "Are two instances of SomeError::OtherError equal?" Assuming ::OtherError is a simple unit-struct variant, yes. The fact that ::OtherError could be used by a library or application as a 'catch-all' to represent a variety of error conditions in the real world is out of scope for this proposal. It is expected that the user of that error type will understand when a comparison is a meaningful operation).
Given that the Error
type is freqently implemented using different types (marker structs, encapsulating structs, enums, and others) the only dependencies we should take are on its API. To that end, I would like to see:
- clarification and distinction between
.description()
and theDisplay
trait for errors - a consistent method to dump the
Error
+ it's.cause()
's - a consistent
.kind()
method since many Errors require it; with a default implementation so that those that do not do not need to worry about it
In order to avoid breaking back-compat, I am thinking if there would have to be a layer (macro?) created (which works on stable
) which, for the purpose of making comparisons, grants consistency to any currently inconsistent standard library errors, and sets guidelines for future error definitions such that they can be compared consistently. It may be possible (I hope it is possible) to leverage error-chain as a foundation for this work in some way.
I'm interested in your thoughts!
Thanks,
Brad