Live data from Hacker News

Rust errors without dependencies

vincents.dev

51–60 of 91 posts

Re: Rust errors without dependencies

#51
> I own the code that I bring into my repo, I belive the standard library is sufficent for my needs without having to pull in more crates.

Unless you're working on something with extremely limited scope, dependencies will become unavoidable; without resorting to reinventing many wheels.

> This is not THE idiomatic way to write rust but rather the way that I write errors. > impl From for DemoError { > fn from(error: std::num::ParseIntError) -> Self { > DemoError::ParseErr(error) > } > }

This introduces a lot of observability risk.

You've essentially built a context eraser. By using a generic From impl with the ? operator, you’re prioritizing brevity during the "happy path" write, but you're losing the "Why" of the error. If my_function has five different string-to-int conversions, your logs will just tell you "Invalid Digit." Good luck grep-ing that in a 100k LOC codebase.

map_err can help fix this, but look at what that does to your logic:

  let my_number: i32 = first_input
      .parse()
      .map_err(|_| 
          DemoError::new(DemoErrorKind::FirstNumberErr(
              first_input.into() 
          ))
      )?;
In a real-world refactor, someone is going to change first_input to validated_input and forget to update the variable inside that closure. Now your error message will report the wrong data. It sends the SRE team down a rabbit hole investigating the wrong input while the real bug sits elsewhere.

And by calling error.to_string() in your Display impl:

  DemoErrorKind::ParseErr(error) => write!(
      f, 
      "error parsing with {}", error.to_string()
  ),
...you are manually "flattening" the error. You’ve just nuked the original error's type identity. If a caller up the stack wanted to programmatically handle a specific ParseIntError variant, they can't. You've turned a structured error into a "stringly-typed" nightmare.

Realistically your risk of mismanaging your boilerplate is significantly higher than a supply chain attack on a crate maintained by the core library team.

Re: Rust errors without dependencies

#52
post #45
post #25

Earlier quoted context omitted.

I think we should provide the building blocks (display, etc like derive_more) rather than a specialized version one for errors (thiserror). I also feel thiserror encourages a public error enum which to me is an anti-pattern as they are usually tied to your implementation and hard to add context, especially if you have a variants for other error types.

I don't quite understand the issue about public error enums? Distinguishing variants is very useful if some causes are recoverable or - when writing webservers - could be translated into different status codes. Often both are useful, something representing internal details for logging and a public interface.

I agree. Is he really trying to say that e.g. errors for `std::fs::read()` should not distinguish between "file not found" and "permission denied"? It's quite common to want to react to those programmatically.

IMO Rust should provide something like thiserror for libraries, and also something like anyhow for applications. Maybe we can't design a perfect error library yet, but we can do waaay better than nothing. Something that covers 99% of uses would still be very useful, and there's plenty of precedent for that in the standard library.

Re: Rust errors without dependencies

#53
post #5

I still think it's kind of mad that the standard library doesn't have better options built in. We've had long enough to explore the approaches. It's time to design something that can go into std and be used by everybody. As it is any moderately large Rust project ends up including several different error handling crates.

A similar problem happened with logging in Java. For many years, there were several competing libraries for logging. Eventually, the community converged on basically none of them, and it became a convention that libraries should use an implementation-agnostic logging API (slf4j), allowing applications to use their preferred implementation (which has a "bridge" to slf4j, e.g. log4j, logback etc.).

Eventually, the JDK did add a logging facility [1]... but too little, too late: nobody uses that and any library that uses logging will probably forever use slf4j.

[1] https://docs.oracle.com/en/java/javase/11/core/java-logging-...

Re: Rust errors without dependencies

#54
post #45

Earlier quoted context omitted.

I don't quite understand the issue about public error enums? Distinguishing variants is very useful if some causes are recoverable or - when writing webservers - could be translated into different status codes. Often both are useful, something representing internal details for logging and a public interface.

I agree. Is he really trying to say that e.g. errors for `std::fs::read()` should not distinguish between "file not found" and "permission denied"? It's quite common to want to react to those programmatically. IMO Rust should provide something like thiserror for libraries, and also something like anyhow for applications. Maybe we can't design a perfect error library yet, but we can do waaay better than nothing. Somet…

I doubt epage is suggesting that. And note that in that case, the thing distinguishing the cause is not `std::io::Error`, but `std::io::ErrorKind`. The latter is not the error type, but something that forms a part of the I/O error type.

It's very rare that `pub enum Error { ... }` is something I'd put into the public API of a library. epage is absolutely correct that it is an extensibility hazard. But having a sub-ordinate "kind" error enum is totally fine (assuming you mark it `#[non_exhaustive]`).

Re: Rust errors without dependencies

#55

Earlier quoted context omitted.

I don't see any reason for something like `rootcause` to become foundational. Most errors are a linear chain and that's good enough for most use cases. It's correct to say that `std::error::Error` does not support a tree of errors. But it is incorrect to say what you said: that it's pointless and doesn't allow error accumulation . It's not pointless and it does provide error accumulation. Saying it doesn't is a broad…

There's a semantics discussion about whether progressively adding context to errors (forming an error chain) counts as "error accumulation" or if error accumulation means collecting several errors that occurred and returning that to the caller. But at this point I think you understand what I meant and I understand your meaning. I do think that `std::error::Error` is mostly pointless. That's a value judgment, and reas…

> I do think that `std::error::Error` is mostly pointless. That's a value judgment, and reasonable people can disagree.

I took it as a statement of fact. It is a factual matter of whether `std::error::Error` has a point to it or not. And it definitively does. I use the error chain in just about every CLI application I've built. It's not even mostly pointless. It's incredibly useful and it provides an interoperable point for libraries to agree upon a single error interface.

And one `Error::provide` is stable, it will be even more useful.

> I've tried to argue that, it can create bigger problems then it solves. It's a trait that only exists on platforms with `std`. That itself is pretty nasty and if you care at all about platforms that aren't like that, you're taking on a lot of build complexity. If you really need this why not just make your own `trait HasCause` which is like a subset of `std::error::Error` functionality, and simply doesn't require `std`?

The `Error` trait has been in `core` for about a year now. So you don't need any build complexity for it.

But you're also talking to someone who does take on the build complexity to make `std::error::Error` trait implementations only available on `std`. (Eventually this complexity will disappear once the MSRV of my library crates is new enough to cover `core::error::Error`.) But... there really isn't that much complexity to it? It's a very common pattern in the ecosystem and I think your words are dramatically overstating the work required here.

> 1) Why does it require `Display` and then not use it?

Because it defines the contract of what an "error" is. Part of that contract is that some kind of message can be generated. If it didn't require `Display`, then it would have to provide its own means for generating a message. It's not a matter of whether it's "used" or not. It's defining a _promise_.

> 2) Displaying it is very simple: `format!("{err}")`. If you want to format the error and it's chain of causes, actually using the `std::error::Error` functionality, the recommended way was to use yet another experimental `error_chain` library. When should we actually do that? When is that appropriate?

Who says it was "the recommended way"? I never recommended `error_chain`.

Writing the code to format the full chain is nearly trivial. I usually use `anyhow` to do that for me, but I've also written it myself when I'm not using `anyhow`.

> Now we have a place where there's two different ways to do the same thing (display an error). Additionally there is controversy and churn around it.

Yes, this is a problem. If something appears in the `Display` of your error type, then it shouldn't also appear in your `Error::source`. This is definitely a risk of getting this wrong if you're writing your error type out by hand. If you're using a library like `thiserror`, then it's much less likely.

> I understand that there's a bright shiny future that people hope it's headed for, where everything around `std::error::Error` is easy and obvious, and we have powerful flexible expressive ergonomic error handling. I was excited about that like 7 years ago, now I just kinda want to change the channel. I'm glad some people still find some benefit in the small improvements that have occurred over time... and I hope in 8 more years there's more to the story than where we are today.

I was very happy with error handling in Rust at 1.0 personally.

I think people got burned by the churn of the error library treadmill. But you didn't have to get on that treadmill. I think a lot of people did because they overstate the costs of write-once boiler plate and understate the costs of picking the wrong foundation for errors.

Re: Rust errors without dependencies

#56

Earlier quoted context omitted.

I agree. Is he really trying to say that e.g. errors for `std::fs::read()` should not distinguish between "file not found" and "permission denied"? It's quite common to want to react to those programmatically. IMO Rust should provide something like thiserror for libraries, and also something like anyhow for applications. Maybe we can't design a perfect error library yet, but we can do waaay better than nothing. Somet…

I doubt epage is suggesting that. And note that in that case, the thing distinguishing the cause is not `std::io::Error`, but `std::io::ErrorKind`. The latter is not the error type, but something that forms a part of the I/O error type. It's very rare that `pub enum Error { ... }` is something I'd put into the public API of a library. epage is absolutely correct that it is an extensibility hazard. But having a sub-or…

It's not uncommon to have it on the error itself, rather than a details/kind auxiliary type. AWS SDK does it, nested even [0][1], diesel[2], password_hash[3].

[0] https://docs.rs/aws-smithy-runtime-api/1.9.3/aws_smithy_runt... [1] https://docs.rs/aws-sdk-s3/1.119.0/aws_sdk_s3/operation/get_... [2] https://docs.rs/diesel/2.3.5/diesel/result/enum.Error.html [3] https://docs.rs/password-hash/0.5.0/password_hash/errors/enu...

Re: Rust errors without dependencies

#57

Earlier quoted context omitted.

> It was `std` only and linked to a concept of backtraces, which made it a non-started for embedded. It just seemed to me that it was a bad idea ever to use it in a library and that it would harm embedded users. It was never linked to backtraces. And if you used `std::error::Error` in a library that you also wanted to support in no-std mode, then you just didn't implement the `std::error::Error` trait when the `std`…

> I wrote about how to do error handling without libraries literally the day Rust 1.0 was published: https://burntsushi.net/rust-error-handling/ > > That blog did include a recommendation for `failure` at one point, and now `anyhow`, but it's more of a footnote. The blog shows how to do error handling without any dependencies at all. You didn't have to jump on the error library treadmill. (Although I will say that `a…

To clarify, I'm on libs-api. I've been on it since the beginning. Stabilizing `std::error::Error` was absolutely the right thing to do. There were oodles of things in Rust 1.0 that weren't stable yet that embedded use cases really wanted. There are still problems here (like I/O traits only being available in `std`). The zeitgeist of the time---and one that I'm glad we had---was to ship a stable foundation on which others could build, even if there were problems.

But also, to be clear, `core::error::Error` has been a thing for over a year now.

> And the benefit of shipping a janky version of `std::error::Error` however many years ago, almost all of which got deprecated, seems hard to put a finger on.

Again, I think you are overstating things here. Two methods were deprecated. One was `Error::description`. The other was `Error::cause`. The latter has a replacement, `Error::source`, which does the same thing. And `Error::description` was mostly duplicative with the `Display` requirement. So in terms of _functionality_, nothing was lost.

Shipping in the context of "you'll never be able to make a breaking change" is very difficult. The downside with embedded use cases was known at the time, but the problems with `Error::description` and `Error::cause` were not (as far as I remember). The former was something we did because we knew it could be resolved eventually. But the APIs that are now deprecated were just mistakes. Which happens in API design. At some point, you've got to overcome the fear of getting it wrong and ship something.

Re: Rust errors without dependencies

#58

> I own the code that I bring into my repo, I belive the standard library is sufficent for my needs without having to pull in more crates. Unless you're working on something with extremely limited scope, dependencies will become unavoidable; without resorting to reinventing many wheels. > This is not THE idiomatic way to write rust but rather the way that I write errors. > impl From for DemoError { > fn from(error: s…

This particular one is just how I decided to take context. You could easily keep the original error type and add context onto the struct as an additional field. Or an alternative could be to take a string and the error type. The I’m using someone’s library because I don’t trust myself argument doesn’t really track for me.

Re: Rust errors without dependencies

#59

Earlier quoted context omitted.

I agree. Is he really trying to say that e.g. errors for `std::fs::read()` should not distinguish between "file not found" and "permission denied"? It's quite common to want to react to those programmatically. IMO Rust should provide something like thiserror for libraries, and also something like anyhow for applications. Maybe we can't design a perfect error library yet, but we can do waaay better than nothing. Somet…

I doubt epage is suggesting that. And note that in that case, the thing distinguishing the cause is not `std::io::Error`, but `std::io::ErrorKind`. The latter is not the error type, but something that forms a part of the I/O error type. It's very rare that `pub enum Error { ... }` is something I'd put into the public API of a library. epage is absolutely correct that it is an extensibility hazard. But having a sub-or…

Why is it an extensibility hazard (assuming you mark the `pub enum Error` as non-exhaustive)?

I mean I don't see the difference between having the non-exhaustive enum at the top level vs in a subordinate 'kind'.

Re: Rust errors without dependencies

#60

Earlier quoted context omitted.

> I wrote about how to do error handling without libraries literally the day Rust 1.0 was published: https://burntsushi.net/rust-error-handling/ > > That blog did include a recommendation for `failure` at one point, and now `anyhow`, but it's more of a footnote. The blog shows how to do error handling without any dependencies at all. You didn't have to jump on the error library treadmill. (Although I will say that `a…

To clarify, I'm on libs-api. I've been on it since the beginning. Stabilizing `std::error::Error` was absolutely the right thing to do. There were oodles of things in Rust 1.0 that weren't stable yet that embedded use cases really wanted. There are still problems here (like I/O traits only being available in `std`). The zeitgeist of the time---and one that I'm glad we had---was to ship a stable foundation on which ot…

> At some point, you've got to overcome the fear of getting it wrong and ship something.

Also Editions mean we can go back and fix things "for the future" in some cases if that's worth the price. For example I believe it's worth the price for the built-in ranges to become IntoIterator, indeed I hoped that could happen in the 2024 edition. It was, I think we'd both agree, worth it to fix arrays in 2021.

This is one of the less celebrated but more important wins of Rust IMO because it not only unlocked relatively minor direct benefits it licensed Rust's programmers to demand improvements, knowing that small things were possible they wanted more.

Post reply on HN