While I fully expected to have difficulty switching from the paradigm of raising and catching exception to checking return values, I wasn't ready to Rust requiring so much code to implement it properly (i.e., by the bible).
So this is the one where I complain…
Non-complaint
I won't be talking about exceptions vs. return values per se. For a language that won't let you omit cases in a match
and where type safety is paramount it totally makes sense to make programmers deal with errors explicitly. Even if it's just saying "drop everything on the floor right here", it's done with an explicit call to panic!
or unwrap()
so you can go over them later with a simple text search and replace with something more sensible.
So if you're coming to it from a dynamic language, like I am, my best advice is to not get upset every time when you have to stop and reset the pipeline inside your brain to think about every little unwelcome Result::Err
that just refuses to go away. Get used to it :-)
As a result, my code changed a lot in myriad of places (not even counting a whole new "errors" module). And it prompted me to consider enforcing tighter invariants on the module boundaries. For example, I now see that instead of dispatching lexemes as Vec<u8>
leaving handling potential utf-8 conversion errors to multiple consumers, it's better to contain conversion within the lexer module and only handle this kind of errors in one place (I haven't done it yet).
Boilerplate
To the bad part, then…
It is imperative that a library should wrap all the different kinds of errors that might occur within it into an overarching library-specific error type. Implementing it in Rust is straightforward but very laborious.
This is my Error type:
pub enum Error {
IO(io::Error),
Utf8(str::Utf8Error),
Unterminated,
Escape(String),
Unexpected(String),
MoreLexemes,
Unmatched(char),
AdditionalData,
}
To make it actually useful I had to:
- For all eight variants write a line converting it into a string for display purposes (
Display::fmt
). All of those lines are unsurprisingly similar looking. - Associate with all of them a short textual description that is slightly different from the one above for no apparent reason.
- For the two first variants that wrap lower level errors I had to explicitly write logic saying that their wrapped errors are in fact their immediate causes.
- For the same two lower level errors I had to explicitly state that they are convertible into my
Error
type using those two first variants. That means a separate single-method 4-lineimpl
for each.
This took 62 lines of mostly boilerplate and repetitive code.
I do feel though that all of this not only should, but could be implemented as some heavily magical #[derive(Error)]
macro, at least to an extent. Might be a good project in itself…
There is no try!
The try!
macro goes a long way towards releaving you from a burden of doing an obvious thing most of the time you encounter an unexpected error, namely returning it immediately up the stack:
fn foo() -> Result<T> {
let x = try!(bar()); // checks if bar() resulted in an error and `return`s it, if yes
// work with an unwrapped value of x safely
}
However, since it expands into code containing return Result::Err(...)
it only works inside a function that returns Result
.
Alas, the core method of Iterator — next()
— is defined to return a different type, Option
. Which means that you can't use try!
if you're implementing an iterator. So I had to write my own local variety — itry!
.
Stopping iterators
Another problem with iterators is that the process doesn't automatically stop upon receving an error code from the latest iteration. This seems natural to me although I have to say that the good folks at the Rust users forum are convincing me that my case is probably not general.
Anyway, I know that my iterators certainly should stop upon the error so I had to implement a simple wrapper to watch the iterator for errors and start returning None
s from then on.
Comments: 2
It looks stupid to create enum constructor for each cause type. Why can't whoever calls your function make case analysis basing on cause type in box error?
A a boxed Error trait object won't give a caller any type information to distinguish between different types of errors (
Error.cause()
also returns a trait reference, not a concrete type). If the only thing they want from an error is to display it, it's fine, but I'd rather not make this decision for them.