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:

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 Nones from then on.

Comments: 2 (feed)

  1. Suor

    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?

  2. Ivan Sagalaev

    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.

Add comment

Format with markdown