logoalt Hacker News

Rust errors without dependencies

44 pointsby vsgherziyesterday at 4:12 AM70 commentsview on HN

Comments

morning-coffeeyesterday at 9:04 PM

> I want less code. I want to limit the amount of 3rd party code I pull in. This is mostly due to supply chain disasters over on NPM scaring me and the amount of code dependencies bringing in see rust dependencies scare me.

`anyhow` has exactly one optional dependency (backtrace). `thiserror` has three (proc-macro2, quote, syn) which are at the base of practically the entire Rust ecosystem.

Unless the author has zero dependencies in general, I'll bet they have all of the above dependencies already.

¯\_(°ペ)_/¯

show 3 replies
athrowaway3zyesterday at 8:50 PM

My controversial take on Rust errors is to use anyhow everywhere until you have an immediate demand for explicit Enums. YANGNI

The pros for using anyhow are big: Easily stack errors together - eg file.open().context(path) -, errors are easily kept up to date, and easy to find where they occur.

An enum error is pleasing when you're finished, but cumbersome in practice.

It's niche situation that you need to write a function that exposes a meaningful Error state the caller can branch on. If the return state is meaningful, you usually don't put it in the Err part. eg parsers using { Ok,Incomplete,Error }. IMO Error enums are best for encoding known external behavior like IO errors.

For example: The Sender.send_deadline returning { Timeout, Closed } is the exception in being useful. Most errors are like a Base64 error enums. Branching on the detail is useless for 99.99% of callers.

i.e. If your crate is meant for >1000 users, build the full enum.

For any other stuff, use anyhow.

show 3 replies
j1eloyesterday at 8:33 PM

That's quite a good amount of boilerplate to create a custom, project-specific handling of errors, which itself can have bugs. During reading I thought "anyhow, at this point you are half way to reinvent the wheel and write your own "anyhow'".

I agree with avoiding an explosion of dependencies; but not at any cost. In any case if custom error handling works, then why not. It's just that it feels like a deviation to do extra work on designing and implementing an ideal error handling system with all the cool features, instead of spending that same time working on the actual target of the project itself.

show 2 replies
cloudheadyesterday at 9:51 PM

The fact that you either need a third party dependency or a large amount of boilerplate just to get decent error reporting, points to an issue in the language or std library design.

I've started also dropping `thiserror` when building libraries, as I don't want upstream users of my libraries to incur this additional dependency, but it's a pain.

show 2 replies
drnick1yesterday at 8:36 PM

> I want less code. I want to limit the amount of 3rd party code I pull in. This is mostly due to supply chain disasters over on NPM scaring me and the amount of code dependencies bringing in see rust dependencies scare me.

And this is basically why I like the C/C++ model of not having a centralized repo better. If I need some external piece of software, I simply download the headers and/or sources directly and place them in my project and never touch these dependencies again. Unless somehow these are compromised at the time of download, I will never have to worry about them again. Also these days I am increasingly relying on LLMs to simply generate what I need from scratch and rely less and less on external code.

show 6 replies
IshKebabyesterday at 8:38 PM

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.

show 3 replies
zahlmanyesterday at 8:35 PM

> In the recent Cloudlfare outage Cloudlflare's proxy service went down directly due to an unwrap when reading a config file. Me and many other developers jumped the shark, calling out Cloudflare on their best practices. In Cloudflare's defense they treated this file as trusted input and never expected it to be malformed. Due to circumstances the file became invalid causing the programs assumption's to break.

"Trusted" is a different category from "valid" for a reason. Especially if you're working in a compiled language on something as important as that, anything that isn't either part of the code itself or in a format where literally every byte sequence is acceptable, should be treated as potentially malformed. There is nothing compiling the config file.

> Why is this better than NodeJS

... That feels like it really came out of nowhere, and after seeing so much code to implement what other languages have as a first-class feature (albeit with trade-offs that Rust clearly wanted to avoid), it comes across almost as a coping mechanism.

show 3 replies
nateb2022yesterday at 10:01 PM

> 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<std::num::ParseIntError> 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.

show 1 reply
TZubiriyesterday at 9:08 PM

>Rust error handling is a complex topic mostly due to the composability it gives you and no "blessed way" to accomplish this from the community.

I find it hard to believe. Since a huge class of errors are caught by compile time static analysis, you don't really need an exception system, and errors are basically just return values that you check.

It's much more productive just to use return values and check them, wrap return values in an optional, do whatever. Just move on, do not recreate the features of your previous language on a new language.

osiris88yesterday at 8:54 PM

OP is spot on, no deps is the way.

I've been using rust for 8+ years, I remember the experiments around `failure` crate, a precursor to anyhow if I remember right... and then eyre, and then thiserror...

It just felt like too much churn and each one offered barely any distinction to the previous.

Additionally, the `std::error::Error` trait was very poorly designed when it was initially created. It was `std` only and linked to a concept of backtraces, which made it a non-starter 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.

And the upside for non-embedded users was minimal. Indeed most of it's interface since then has been deprecated and removed, and it to this day has no built-in idea of "error accumulation". I really can't understand this. That's one of the main things that I would have wanted an generic error interface to solve in order to be actually useful.

It was also extremely painful 5 years ago when cargo didn't properly do feature unification separately for build dependencies vs. target dependencies. This meant that if you used anything in your build.rs that depended on `failure` with default features, and turned on `std` feature, then you cannot use `failure` anywhere in your actual target or you will get `std` feature and then your build will break. So I rapidly learned that these kinds of crates can cause much bigger problems than they actually solve.

I think the whole "rust error handling research" area has frankly been an enormous disappointment. Nowadays I try to avoid all of these libraries (failure, anyhow, thiserror, etc.) because they all get abandoned sooner or later, and they brought very little to the table other than being declared "idiomatic" by the illuminati. Why waste my time rewriting it in a year or two for the new cool flavor of suck.

Usually what I actually do in rust for errors now is, the error is an enum, and I use `displaydoc` to make it implement `Display`, because that is actually very simple and well-scoped, and doesn’t involve std dependencies. I don't bother with implementing `std::error::Error`, because it's pointless. Display is the only thing errors need to implement, for me.

If I'm writing an application and I come to a point where I need to "box" or "type erase" the error, then it becomes `String` or perhaps `Box<str>` if I care about a few bytes. It may feel crude, but it is simple and it works. That doesn't let you downcast errors later, but the situations where you actually have to do that are very rare and I'm willing to do something ad hoc in those cases. You can also often refactor so that you don't actually have to do that. I'm kind of in the downcasting-is-a-code-smell camp anyways.

I'm a little bit excited about `rootcause` because it seems better thought out than it's progenitors. But I have yet to try to make systematic use of it in a bigger project.

show 1 reply
hullopayesterday at 9:15 PM

[dead]

llmslave2yesterday at 9:04 PM

[flagged]

show 2 replies