Live data from Hacker News

A guide to error handling in Rust

nrc.github.io

31–40 of 76 posts

Re: A guide to error handling in Rust

#31
post #27

Too bad Rust doesn't have union types (aka adhoc / anonymous unions) yet. Without them, using typed errors is very clumsy. Optimally, you would write the following code: fn foo(r1: Result , r: Result ) { let i1 = r1?; let i2 = r2?; // ... } and Rust would infer the return type to be Result without having to do any extra definitions or conversions.

I know that this is a common wish, but anonymous sum types have pretty catastrophic impacts on type checking and lead to all sorts of bizarre corner cases like the following: let a = if cond { 1 } else { 1.0 }; a + 3 Now, the error would be pushed to the `+` operator because there isn't an `Add` for `f32 | u32`. Granted, this is a trivial example, and a programmer can easily see through it, but in general this can ge…

> Now, the error would be pushed to the `+` operator because there isn't an `Add` for `f32 | u32`.

Why not? It makes total sense for one to exist. This is something the language needs to deal with, not the programmer. But the programmer always sprinkle annotations so that errors can only reach so far.

Re: A guide to error handling in Rust

#32

Too bad Rust doesn't have union types (aka adhoc / anonymous unions) yet. Without them, using typed errors is very clumsy. Optimally, you would write the following code: fn foo(r1: Result , r: Result ) { let i1 = r1?; let i2 = r2?; // ... } and Rust would infer the return type to be Result without having to do any extra definitions or conversions.

For anyone interested in what this would look like in Rust now, there's two ways. For libraries, people tend to recommend the thiserror crate. Code sample[0]: #[derive(thiserror::Error, Debug)] enum Error { #[error("One")] One(#[from] Error1), #[error("Two")] Two(#[from] Error2), } fn foo(r1: Result , r2: Result ) -> Result { let i1 = r1?; let i2 = r2?; // ... } Whereas for binaries, people usually recommend anyhow.…

For simple tasks you can get away without any external crates by using `Result>`. But it's much more comfortable to use thiserror or anyhow in the long run.

    fn read_string() -> std::io::Result {
        Ok("123".to_owned())
    }

    fn main() -> Result> {
        let s = read_string()?; // io::Error
        let n = i32::from_str_radix(&s, 10)?; // num::ParseIntError
        println!("read number: {n}");
        Ok(())
    }

Re: A guide to error handling in Rust

#33

Earlier quoted context omitted.

How would that work in a memory safe language? Rust does have (named) untagged unions already (using the `union` keyword), but they are unsafe to use because there is no way to know statically which of the possible variants a given value contains.

You can obviously only call common methods or have to pattern match later and have a way to tell them apart. If you can't tell them apart, the compiler will tell you and you need to tag them somehow.

> You can obviously only call common methods

That sounds like trait objects/dynamic dispatch/`dyn`, which comes with runtime costs.

> or have to pattern match later and have a way to tell them apart.

That "way to tell them apart" is a tag, which would make it a tagged union/enum, not an untagged union. Those already exist, though not in an anonymous flavour.

Re: A guide to error handling in Rust

#34

Earlier quoted context omitted.

Not sum types. Those are union types. The difference is important, since if you work with two results (or two functions that return results) that use the same error-type you most often don't want to end up with a tuple of two times the same error but simply A . Of course, if you care about which error is from which function, you can always easily do that by wrapping them into a sumtype, but in practice this is a rath…

Unions don't have a discriminant. Anonymous Sum types have a discriminant, you just can't name it. Unions in Rust are unsafe because you can't tell what the underlying value will be.

Well, that depends. If the union consists of two types that share the same underlying structure, then obviously at runtime we can never know what the value is.

But otherwise we can. And this is something that we will know at compile-time, so we can prevent runtime-checks that would not work.

Re: A guide to error handling in Rust

#35

Earlier quoted context omitted.

You can obviously only call common methods or have to pattern match later and have a way to tell them apart. If you can't tell them apart, the compiler will tell you and you need to tag them somehow.

> You can obviously only call common methods That sounds like trait objects/dynamic dispatch/`dyn`, which comes with runtime costs. > or have to pattern match later and have a way to tell them apart. That "way to tell them apart" is a tag, which would make it a tagged union/enum, not an untagged union. Those already exist, though not in an anonymous flavour.

It does, but the developer decides. Also, errors are often propagated and only matched in exceptional cases, so I don't think the impact would be big.

> That "way to tell them apart" is a tag, which would make it a tagged union/enum, not an untagged union.

No. The difference is that a tagged union (sum type) is defined in advance but generally a union is not necessarily tagged but _can_ be tagged.

Example: say you have tagged union with 3 different types / tags A, B and C. You can now define an adhoc/untagged union that is A | C. That means, we can guarantee at compile time, that we will be able to tell A and C apart later. But it is still not the same, because the combination of A and C was decided adhoc and was not predefined by the developer anywhere necessarily - which is what makes it different from A, B, C which where specifically defined by the developer.

Re: A guide to error handling in Rust

#36

I feel like this document makes the Try operator (?) and its associated trait more mysterious than necessary. Most people probably won't need to implement Try, especially before it is stabilised, but it's not that much more complicated than say, AddAssign the trait which you implement to make the Add Assignment (+=) operator work on your type. The key trick of Try is that it converts something (by default an Option o…

BTW if you have read about the `Try` trait before and are wondering what `ControlFlow` is, read it again: https://doc.rust-lang.org/nightly/std/ops/trait.Try.html

`Try` was recently changed significantly with the introduction of `ControlFlow`. IMO it's a big improvement.

Re: A guide to error handling in Rust

#37
post #14

It is perhaps too verbose by default, as indicated by popularity of thiserror and anyhow crates.

Perhaps, but it's also much better than it was two years ago and there is work going on to make it better two years in the future.

A myriad of experimental prototypes (like the failure crate and its descendants) have been made, experimented with and then retired and looks like the progress is converging to these two complementary error handling crates (anyhow, thiserror, and a few mostly-compatible variants like eyre), and work going on to standardize some aspects of it so (parts of) these crates can be retired. There's also core::error that's bringing this to no-std environments.

So yeah, it definitely was not great on day 1 and there's been a lot of churn on error handling but it is going in the right direction.

Re: A guide to error handling in Rust

#38
post #30

This glaring omission from this is the "enum idiom": https://doc.rust-lang.org/std/convert/trait.From.html#exampl... they talk about it here: https://nrc.github.io/error-docs/error-design/error-type-des... but including more than a snippet would go a long way to that "aha" moment I think. This was frustrating for me browsing this site. The author wrote 10 pages of docs, but nearly all the examples are like 5 line sni…

Whats the "aha" moment for it, the Froms?

The author might not have included that as they call out you likely shouldn't directly wrap another error.

I go a step further and think that public errors shouldn't have From's for concrete types, exposing your implementation details, and that enum errors are more generally too tied to implementation details to be used in libraries.

Re: A guide to error handling in Rust

#39

Earlier quoted context omitted.

Even if the "Ad hoc union" becomes a thing in Rust, you are not likely to get inference of return types. The return type is part of the function signature and Rust deliberately doesn't infer signatures, in languages with "too much" inference it's impractical for the human programmer to keep track of types because it's all inferred, this has started to be a problem in C++ as more and more things are auto. Rust has som…

I think it's the job of the IDE to make that work, but I agree, without IDE this can quickly become a problem.

I think method signatures are part of application/typesystem design and should not be inferred. Explicitly provided types are a feature. Inferred/auto type signatures are "necessariy evil" to reduce boilerplate type declarations around code.

While codeblocks `fn1(fn2(), fn3)` and `var r1 = fn2(); var r2 = fn3; fn1(r1, r2)` are more or less identical, unless you have static type definitions for these methods you start having a very bad time inferring what types are being passed around.

Consider typical python wrapper library with liberal use of *kwargs to pass non-wrapped arguments down to wrappee. Those arguments and their types (as much as they are available in python, you get the idea) are entirely missing from wrapper code and make changes at call site pretty difficult

Re: A guide to error handling in Rust

#40
post #27

Earlier quoted context omitted.

I know that this is a common wish, but anonymous sum types have pretty catastrophic impacts on type checking and lead to all sorts of bizarre corner cases like the following: let a = if cond { 1 } else { 1.0 }; a + 3 Now, the error would be pushed to the `+` operator because there isn't an `Add` for `f32 | u32`. Granted, this is a trivial example, and a programmer can easily see through it, but in general this can ge…

> Now, the error would be pushed to the `+` operator because there isn't an `Add` for `f32 | u32`. Why not? It makes total sense for one to exist. This is something the language needs to deal with, not the programmer. But the programmer always sprinkle annotations so that errors can only reach so far.

> It makes total sense for one to exist.

I disagree: IME, when you add a float and an integer, you want to cast float to integer 50% of the time, and integer to float the remaining 50% of the time.

Even if it leads to more verbosity, I prefer arithmetic operations to be endomorphisms and use explicit casts.

Post reply on HN