top | item 19721998

Things I Enjoy in Rust: Error Handling

118 points| ingve | 7 years ago |blog.jonstodle.com | reply

85 comments

order
[+] zmmmmm|7 years ago|reply
It's interesting to see modern languages tackling checked error handling. Particularly in the light that Java's checked exceptions are widely considered to have been a failure. Yet I wonder if Java had better support for things like union types etc. if it would have turned out differently. After all, it did offer at least potential to achieve what everyone is after: you can write a piece of code and know for sure that you handled every possible type of error.

The most painful aspect of Java's (original) exceptions was how the only mechanism to handle multiple errors as a single case was using inheritance in the exception hierarchy. The result was that they propagated through interfaces and broke downstream code and it became pretty much impossible to design and maintain a stable API without wrapping exceptions excessively which in turn destroys the utility of the exception hierarchy, making the whole exercise futile.

The article seems a little superficial though. Immediately exiting with 'panic' does not seem like a real error handling approach for any realistic application. It doesn't show how you could handle different types of errors (eg: file not found vs I/O error reading file) with different cases. The case it does present is easily handled elegantly by nearly any error handling design.

[+] int_19h|7 years ago|reply
I think the biggest problem with Java exception specs is that they're not composable. In other words, I can't define a method X that calls another method Y - potentially via dynamic dispatch - and has an exception specification of "whatever Y throws, plus E".

This manifests almost immediately. For example, suppose you wanted to implement a key/value database with disk storage. The logical core interface for it would be java.util.Map, except for the fact that its methods are declared as non-throwing; thus, you cannot propagate IOException from the underlying storage layer, unless you wrap it into unchecked RuntimeException.

Conversely, java.lang.Appendable is a very generic interface for appending characters or sequences of characters to something. But because its designers knew that some of its implementations would be backed by I/O, all mutating methods in that interface are declared as throwing IOException. Thus, any generic code that tries to work with Appendable has to always assue that it can throw, and has to declare itself as throwing the same exception to propagate. And now the user of that generic code passes a no-throw implementation of Appendable to it, but still has to "handle" IOException that they know will never actually happen.

Or come up with some convoluted scheme, like java.util.Formatter does. This one wraps Appendable, but swallows all I/O exceptions to avoid having to propagate them in the most common case, where the wrapped Appendable is a StringBuilder. But since there are other possibilities, it still has to expose some API to examine such exceptions, which it does by providing a method on Formatter that returns the last swallowed exception that was thrown by Appendable. Of course, now you have to remember to actually check it, and you will silently get an incorrect result if you do not, which is worse than unchecked exceptions in the same scenario.

This all gets especially bad one you start working with HOFs, because their contracts are largely defined in terms of the behavior of functions that are passed to them. The old Java lambda proposal by Neal Gafter noted this, and proposed extending the syntax for generics to cover exception specifications (via union typing, indeed). In the end, they added UncheckedIOException to the standard library, specifically so that stuff like Stream.map() can be implemented on top of files, and propagate exceptions, without declaring them.

[+] uzername|7 years ago|reply
I've shown Rust's Result type to folks on occasion because I like it so much. The other aspect I like is the `?` operator, which allows for easier error propagation.

https://doc.rust-lang.org/book/ch09-02-recoverable-errors-wi...

[+] 59nadir|7 years ago|reply
I think propagating null operators, `?`, etc., all become a bit less interesting when you consider that mostly they're just specific implementations of a monadic interface and they could all just be represented like that instead. It doesn't seem sensible to me to try to "fake" monads everywhere and add different operators for different ones when we could just have one interface that describes short-circuiting based on the data type.
[+] fnord77|7 years ago|reply
`?` isn't really that great - for all but toy cases you have to jump through hoops - especially if you are trying to handle different error types from separate in a unified way.

Not a fan of Result, either - it ties together the error type and the type of the desired item - these should be separate concerns.

[+] wyldfire|7 years ago|reply
One of the more interesting semi-recent additions to stable rust is the question mark operator. It's not covered in this Rust Error Handling article, but it's got some of the early-exit appeal of exceptions without too much unwind magic.

Unfortunately I haven't been able to get the question mark operator to work yet with my code, but it looks appealing. It's not 100% obvious to me what qualities my Result type needs in order to be able to make it work. Or whether it's bound in practice to the Result type(s) defined in std. I'm sure those details are all in the docs somewhere but I spent the time to look yet.

[+] CUViper|7 years ago|reply
One thing you may be missing is that the `Result::Err` in your return type must implement `From` for the `Result::Err` type where you're using the `?` operator. That's already covered if those `Err` types are identical (`From<T> for T`), otherwise you need to implement the conversion.

    impl From<QuestionError> for ReturnError {
        fn from(error: QuestionError) -> ReturnError {
            // TODO
            unimplemented!()
        }
    }
[+] rabito|7 years ago|reply
Also, if you use Box<Error> or failure[1]'s Error type you can get around implementing most of the conversion Traits, but you lose the ability to tell which errors can happen by just looking at the type signature and doing exhaustive matching on them.

[1]: https://github.com/withoutboats/failure

[+] csomar|7 years ago|reply
If you can post your code, we can help you out. One requirement, is that your function returns a Result<Ok, Err>. The Err needs to implement the same type for all the type returned.

This means you can use the "?" operator for functions that you did not create (libraries) but you'll also have to convert their Error type to your Error type.

[+] dep_b|7 years ago|reply
I'm kind of contemplating a career switch since in mobile JavaScript cross-platform seems to become pretty big and I just don't feel very warmly about becoming a JavaScript specialist.

What are the possibilities for somebody that's pretty heavy iOS senior to transfer to a role building applications or services in Rust (or Elixir, for that matter)?

[+] rhinoceraptor|7 years ago|reply
AFAICT, 99% of the Rust jobs out there are just crypto startups. So at this point, not that great.
[+] ansible|7 years ago|reply
We (as an organization) are a couple years away from doing Rust for embedded work, but I'll be pushing hard in that direction after I'm more comfortable with it (I'm the tech lead in the organization), in part because I dislike C++ so, so much.
[+] onebot|7 years ago|reply
I would consider digging into WASM. I think there will be a lot of demand eventually and Rust has a good story there.

Rust is slowly gaining momentum. Jobs are starting to pop up here and there.

[+] icxa|7 years ago|reply
I am a full time Go developer learning rust on the side for more low level things, and one of my side projects is writing an OS.

Rust's error handling really feels the exact same way to Go's in my opinion, the only difference is, so far in my admittedly newcomer viewpoint, using unwrap to get to the error value, or returning an error value and doing an if err != nil check.

This isn't to say anything other than agreement that I indeed like this style. I like to handle my errors as values and program around them.

The thing I don't get is why all the bikeshedding and hate around "if err != nil" spam I seem to have seen here often and on other blogs. Go seems to get this "criticism" (I don't really believe it is a valid critique, for what it is worth) heaped onto it plentifully, while Rust seems generally immune to it, otherwise praised for the approach -- but it is the same approach! In fact you even have a builtin panic func in go!

Can someone tell me what I may be missing?

[+] ohazi|7 years ago|reply
A lot of the Rust developers I've spoken with don't really consider using unwrap everywhere the way that Go programmers use "if err != nil" to be proper error handling. Most of them consider it an ugly hack to be used when playing around or testing something.

The value that most people see in Rust's error types is in error transformation and propagation, which let you do things like correctly handling edge cases when using closures without cluttering up the logic of the success case.

If you are going to compare unwrap/expect to "if err != nil" -- Rust's advantage is that it forces you to at least pretend to handle the error, while Go will happily keep chugging along even if you never inspect err.

[+] anuragsoni|7 years ago|reply
While its true that one can use `unwrap`, pattern matching and other higher order methods available for the Result type are a more idiomatic way to work with these.

For example:

    match <something that returns result> {
        Ok (v) => <do something with v>,
        Err (some error) => <handle error>
    }
By using the result type, the compiler ensures that the only way to get a value out of the result type is via pattern matching. So the errors are still values, except there is no need to come up with a default value for the error scenario, and there is no need to come up with an "empty" value.

In isolation it might look verbose, but rust also provides a lot of higher order operations that one can do on result types. Ex:

    myResult.map(<do-something>).and_then(sq).or_else(<do-something-else>);
Something else that I personally like about a result type is that it also serves as documentation by clearly marking operations that can fail, and have a consistent way for the end-user to know which value indicates that the response is a success or an error.

EDIT: Formatting

[+] sanderjd|7 years ago|reply
In Go, you don't have to check err != nil to get the underlying value, in Rust you do. You probably aren't missing that exactly, in that you probably knew that difference, but you may be missing its importance. It means that in Rust, it is diminishingly rare to have a value of some type which it is not valid to use, whereas in Go many or most values - any value returned from a function that also returns an error, which is a lot of them - may be invalid (because the error may not have been checked). In mature teams using Go it may be unlikely for any errors to be unchecked, but it is a gun that is always pointed at your foot.
[+] mixedCase|7 years ago|reply
Since Rust supports generics and algebraic data types, you can clearly denote on your return type what kind of errors can be expected and include useful data pertaining to each one, with the compiler making sure everything in its place; as opposed to casting from the error interface in a brittle manner prone to breaking on changes with no help from the compiler.

Since Result is also a functor in practice, you can chain modifications to the happy path without having to check errors when you don't need to and without having to write as much boilerplate.

[+] joshmarlow|7 years ago|reply
You can do `.unwrap()` or `.expect(..)` to get your values, and in that respect, it's not that different from working in a language with `null`.

But you can actually almost totally avoid calling `unwrap`/`expect` in Rust code by doing pattern matching.

` match x { Some(value) => <DO SOMETHING WITH VALUE> None => <HANDLE THIS ERROR PATH>, }`.

If you always do that, then you've got the compiler reminding you to handle the case where it is `null`. That means that you never hit the equivalent of a null exception.

That being said, I still find myself doing some `unwrap` and `expect` (particularly in tests). But relying on matching most of the time as a first line of defense will prevent the equivalent of a null exception in the bulk of your code.

[+] andrewjf|7 years ago|reply
The best parts of rust's error handling are the try!() macro (aka `?`), as well as From/Into error types allow the compiler to wrap types or convert between error types. This eliminates almost all of the go boilerplate `if err != nil` and you only handle the error where you actually need to with pattern matching and destructuring, instead. It is much nicer. Go's error handling is pretty primitive, in my opinion.
[+] rhinoceraptor|7 years ago|reply
It seems like Go's error handling is basically like node's err callback argument, maybe a little cleaner. But it's still not that strict in forcing you to deal with errors. You could just rename err to _ and just drive on.

In Rust, you either have to deal with the error, or explicitly ignore them. And if the error is the type that warrants crashing, that's easy too.

[+] dnautics|7 years ago|reply
If you'd like to see something that's not "if err != nil", I recommend looking into elixir; there is the option of doing ok/error tuples, and there is a more ergonomic railway programming structure called "with". However, as it is a BEAM language, often the best solution is to "let it fail" as an error handling strategy. Let the process raise and crash, and a supervisor will restart it in a safe state if necessary.
[+] csomar|7 years ago|reply
I don't particularly like the solution presented.

1. The Rust community is biased against the usage of "unwrap". It was supposed to make prototyping easier. It should not have been there and should not be used.

The correct way will be to "evaluate" the function and either return a result or "propagate" the error to some part of your application that will handle "all" the errors. Whether they are your code errors, or errors thrown by other libraries.

2. The code will look even more succinct with the "?" operator:

> let value = some_operation_that_might_fail()?;

3. If the function fail, the program should handle the failure. By design, a failure should not result on setting a "default value". That's not an error but more like an "option".

The correct type the guy should have used is the Option type. Or more like an option inside a result. The option could return a value or not. If not, you set a default value.

[+] epage|7 years ago|reply
> 1. The Rust community is biased against the usage of "unwrap". It was supposed to make prototyping easier. It should not have been there and should not be used.

I'm a little confused on this point you are making. You say the Rust community is biased against `unwrap` which makes it sound like you think that is wrong but then you proceed to say it shouldn't be there, making it sound like you agree.

`unwrap` has legitimate uses in product code as an assertion. Normally I recommend `expect` instead so that the assertion is self-documenting but there are times where the the scope of the assumption you are making is so small, it is obvious and doesn't need a comment, for example if I partition a collection based on the result [0]

[0]: https://doc.rust-lang.org/stable/rust-by-example/error/iter_...

> . If the function fail, the program should handle the failure. By design, a failure should not result on setting a "default value". That's not an error but more like an "option".

From the function's perspective, it errored, but from your perspective, it failed and you want to fallback to a default value.

Yes, someone could express this instead as an `Option` or a `Result<Option, _>`

- Sometimes you just don't need that extra boiler plate - Sometimes you want to provide context for why something failed in case the user considers it an error as well. You can't express that with `Option`.

[+] bfrydl|7 years ago|reply
While I too love Rust's error handling, this explanation of it seems incomplete without discussing the ? operator and its interaction with the From trait. The convenience methods on Result itself are nice of course but they aren't really the main mechanism of error handling in my opinion.
[+] gambler|7 years ago|reply
So what do you do when you want centralized error handling, which is the whole point of try-catch idiom?

>he second side effect is that it forces you to think about how to handle possible errors.

Java does this with throws E and (at least in Java) it sucks and I very much prefer C#'s implicit throws. In fact, I don't think it goes far enough. If you have complex error recovery, the language should allow you to completely separate good executions paths from the bad paths. Scope guards are a step in the right direction.

How would Rust code looks like if the error handling logic was something like "try X, if it fails try again, if that fails schedule another attempt via a queue, log a warning; if scheduling fails log a fatal error"?

Sure, handling this stuff with try-catch looks horrible. But I've thought long an hard about how to simplify it and using tricks like .unwrap_or (which you can simulate in C# to some extent) does not cut it, because error handling often requires to operate on method-scoped variables (e.g. for logging).

[+] ChrisSD|7 years ago|reply
Depends what exactly you're looking to do. You can propagate errors with the `?` operator. And you can match on the specific error at a higher level.

The docs[0] do give a more complex example of error matching:

    fn main() {
        let f = File::open("hello.txt");

        let f = match f {
            Ok(file) => file,
            Err(error) => match error.kind() {
                ErrorKind::NotFound => match File::create("hello.txt") {
                    Ok(fc) => fc,
                    Err(e) => panic!("Tried to create file but there was a problem: {:?}", e),
                },
                other_error => panic!("There was a problem opening the file: {:?}", other_error),
            },
        };
    }
Although they also provide a slightly simpler alternative for this particular case.

[0] https://doc.rust-lang.org/stable/book/ch09-02-recoverable-er...

[+] namelosw|7 years ago|reply
Designs like Java which have NullPointerException simply ignore null. Modeling T instead of T | null is just too convenient. It's a lazy design.

I'm fine with most of those new ways of error handling, although some feel better and the others feel a little bit awkward. If some concept exists (exception/emptiness/async), model it, handle it. Instead of just ignore it and leak the responsibility to the users.

[+] XCSme|7 years ago|reply
Not being familiar with Rust, the article was interesting, but I felt like it lacked examples. The Result<T, E> is also available in other languages, even in TypeScript you could have `function x(): number | Error {}`, it is just a consequence of being able to return a union of data types, why is Rust's case special? (apart from being able to handle it nicer with "match")
[+] guntars|7 years ago|reply
Rust’s is a tagged union so you can have Result<number,number>, something you can’t have in Typescript.
[+] gamma3|7 years ago|reply
Really nice! The Result reminds me of Validation from Scala.

In Scala you return a Validation that is either a value or an error, and can pattern match on it. Also: trySomething().orElse("Default value")

[+] CuriousSkeptic|7 years ago|reply
To be fair. The idiomatic C# would be:

    if(TrySomeOperationThatMightFail(out var value))
    {
    }

But even with that I often wish it was more like the Rust example
[+] snypox|7 years ago|reply
Except 99% of methods are async in a web app where the out variable does not work.
[+] CountHackulus|7 years ago|reply
This is an extremely common pattern in Erlang and is super useful so it's nice to see Rust embrace it.
[+] miohtama|7 years ago|reply
The article is a good opinion, but lacks necessary wider discussion in the context of all programming.

1) Is Rust error handling somehow better than in a programming language X?

2) Is it because of Rust or because standard libraries or something else (developer laziness, writing silent exception eating)

3) Why other programming languages choose to do it other ways?

Now the article reads like Rust education, which is good, but other commenters here find it very subjective.