Live data from Hacker News

A guide to error handling in Rust

nrc.github.io

61–70 of 76 posts

Re: A guide to error handling in Rust

#61
post #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.

> public errors shouldn't have From's for concrete types

> enum errors are more generally too tied to implementation details to be used in libraries

I generally agree. SNAFU addresses these problems in two ways:

1. The `From` implementation is not created for the underlying error but for an intermediate type (by default). That type is private to the crate (by default) and cannot expose implementation details.

2. There's an opaque error facility to completely hide the enum details.

Put together, that looks something like...

    use snafu::prelude::*;
    use std::{
        fs,
        path::{Path, PathBuf},
    };
    
    #[derive(Debug, Snafu)]
    enum ErrorImpl {
        #[snafu(display("Could not read the config file {}", path.display()))]
        UnableToReadConfig {
            source: std::io::Error,
            path: PathBuf,
        },
    
        #[snafu(display("Could not write the config file {}", path.display()))]
        UnableToWriteConfig {
            source: std::io::Error,
            path: PathBuf,
        },
    }
    
    #[derive(Debug, Snafu)]
    pub struct Error(ErrorImpl);
    
    pub type Result = std::result::Result;
    
    pub fn do_stuff_with_config(path: &Path) -> Result {
        let config = fs::read_to_string(path).context(UnableToReadConfigSnafu { path })?;
        fs::write(path, config).context(UnableToWriteConfigSnafu { path })?;
        Ok(())
    }
Other things about SNAFU:

- It's very easy to add valuable context to the errors. See how the `&Path` context is transformed to a `PathBuf` with low ceremony in the example.

- You can create struct- or enum-based errors.

- You can use "stringly-typed" errors (akin to anyhow) but in combination with strongly-typed errors. This allows you to start out with a loose error handling regimen and make it stronger as you go along.

- There's support for capturing backtraces or lightweight file/line/column information.

- There's a pretty error reporter for usage with `main` functions or tests.

- There's support for the nightly-only Provider API.

Re: A guide to error handling in Rust

#62
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…

One option might be to differentiate syntactically between branches of an if/match that are allowed to expand their type (to a union or to a bigger union) and those that are not. I am not sure how far that generalizes, though.

Re: A guide to error handling in Rust

#63

Earlier quoted context omitted.

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…

I like OCaml's approach, where you write types in module signatures, but don't need to put them in the implementation.

Re: A guide to error handling in Rust

#64

Earlier quoted context omitted.

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…

> unless you have static type definitions for these methods you start having a very bad time inferring what types are being passed around. It doesn't matter if the types are annotated explicitly or inferred. The amount of information is 100% the same. The IDE could just fill in the types _exactly_ in the same way as they would look like when annotated by hand - maybe just with a different color. IntelliJ does this qu…

> It doesn't matter if the types are annotated explicitly or inferred. The amount of information is 100% the same.

Explicit offers the opportunity for narrowing, comments, and sometimes choice of names. Otherwise, yes.

Re: A guide to error handling in Rust

#65

Earlier quoted context omitted.

I must not be understanding what you're asking for because it sounds like anonymous sum types.

Maybe this is just about terminology. But essentially, when it comes to union types, they behave like sets. The compiler merges them. (A | B) | (A | B) is the same as A | B. But for sum types (even anonymous ones such as tuples) the compiler can't merge them because that would lose information (if the result is from the first A | B or the second one). Instead, you end up with a nested structure. Which one is desired…

The problem is that if you have e.g.

    let x: u64|i32 = ...
It's impossible for the compiler to do anything with x without adding some kind of runtime type tag. The representations of those types are different.

But with something like OCaml's polymorphic variants, you could do e.g.

    let x: Big(u64)|Small(i32) = ...
and then switch on the tag (Big or Small) to determine what to do with the values.

Re: A guide to error handling in Rust

#66

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.

IIUC this kind of union would discriminate only on type.

Re: A guide to error handling in Rust

#67
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…

Typescript seems to have no problem with it (nor do I suspect F#/OCaml):

  class A {
    x = 10;
    scale(n: number): void {
      this.x *= n;
    }
  }
  class B {
    y = 10;
    scale(n: number): void {
      this.y *= n;
    }
  }  
  var z: A | B;
  z = new A
  z.scale(2);
  z = new B
  z.scale(2);
I was also looking for Sum Types/anonymous enums[0].

[0] https://news.ycombinator.com/threads?id=karmakaze&next=33505...

Re: A guide to error handling in Rust

#68
post #56
post #32

Earlier quoted context omitted.

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(()) }

Except you often need `Result >` if you go that route. At the very least, you should create a type alias for it. I very much prefer the use of `anyhow` and/or `thiserror` depending on if I need typed errors.

And the reason it works in anyhow is ... They have those conversions: https://docs.rs/anyhow/latest/anyhow/struct.Error.html#impl-...

Re: A guide to error handling in Rust

#70

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…

While I don't strongly object to Rust's choice here, and I agree that production code should have a type signature on every function, I think this is more a place for lint/clippy/whatever. There's no need to gate the programmer trying something on them having produced a type signature that could be inferred.

You might be interested to know that rustc does have some limited ability to infer return types[1], but there are so many edge cases that the feature as it exists today isn't perfect (and won't let you produce a binary). I believe that 1) type alias = impl Trait; will make prototyping easier, and 2) we should allow -> _ in return types in private functions so that they become an allowed part of the language but critically can't become a semver hazard in crates' APIs.

[1]: https://play.rust-lang.org/?version=stable&mode=debug&editio...

Post reply on HN