Tags

, ,

I just read the documentation for std::result in Rust. A friend had pointed it out and said it left a sour taste in his mouth, as it does mine. These are essentially error-carrying monads, in case you’re familiar with Haskell terminology. They are error handling mechanism that is added on top of the core language (that is, not part of the fundamental syntax and semantics).

Nicer?

If we scroll down the doc we find this example, followed with the comment “It’s much nicer!”

1
2
3
4
5
6
7
8
fn write_info(info: &Info) -> io::Result<()> {
    let mut file = try!(File::create("my_best_friends.txt"));
    // Early return on error
    try!(file.write_all(format!("name: {}\n", info.name).as_bytes()));
    try!(file.write_all(format!("age: {}\n", info.age).as_bytes()));
    try!(file.write_all(format!("rating: {}\n", info.rating).as_bytes()));
    Ok(())
}

It certainly is more compact than the example they had before, but there is nothing nice about this code. Consider the following instead:

1
2
3
4
5
6
fn write_info(info: &Info) -> {
    let mut file = File::create("my_best_friends.txt");
    file.write_all(format!("name: {}\n", info.name).as_bytes());
    file.write_all(format!("age: {}\n", info.age).as_bytes());
    file.write_all(format!("rating: {}\n", info.rating).as_bytes());
}

If every statement that could produce an error requires a try! then why not just make it a fundamental part of the language? This mindless repetition of try! and the need to declare io::Result<T> for the return just leaves me feeling the language is missing something.

But but but…

The Rust code works because return values cannot be ignored. Returning a std::result forces the caller to deal with it somehow. The example however does nothing but pass the result back to its caller. I would bet that the vast majority of code dealing with std::result will simply do the same thing: pass the result back to the caller.

This is precisely why exceptions were created in languages like C++. They avoid the needless forwarding of errors from function to function, while also provided a way to intercept them when needed.

My reduced form of their example has the same semantic meaning as the first form, but with less syntax. This makes it easier to understand the purpose of the code. It also makes it easier to handle errors correctly; being an implicit part of the language makes all functions, by default, handle errors in a sane manner.

One popular criticism of exceptions is that they have hidden, or extraordinary program flow. I consider that a weak argument, but I’ve heard it used to justify monads before. If that is somehow the motivation for std::result then I should point out it fails here anyway. try! has the same perceived “problem” as exceptions: it is hiding a conditional and return statement.

A much better use of try!

Exceptions are bulky to handle in C++, and in all languages that came after unfortunately. This is one reason why some people dislike them. Rust could instead choose to clean this up and make try! stop the exception propagation and convert it into the std::result instead.

1
2
3
4
    res = try! error_prone_function(...)
    if !res {
        //handle error contained in `res`
    }

This is a compact way of handling errors in situations where we know what to do with them. It still requires something like std::result in order to contain the result or an error, but it flips the default semantics: everything generates and propagates errors by default instead of nothing.

The mantra

Languages must face the reality that most function calls can fail, and only a handful of functions will actually know how to deal with errors. The vast majority of functions will simply proxy errors from their callees to their callers. It only makes sense to design a language around this situation.

Error handling should be an implicit fundamental part of the language. This doesn’t mean it has to use the “throwing” approach of C++ exceptions, that’s just a technical detail. The compiler can just as easily use return value propagation of errors at the low-level. The high-level language shouldn’t really concern itself with “how” they are implemented, but focus on making them easy to use.

Advertisements