> 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.
Anyhow itself is still a dependency. This is more something I wanted to do and not something I recommend for everyone. Google took a similar approach in how they added rust for chrome. They don’t use an error handling library.
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.
Any medium or large C project has these kinds of project-specific (or sometimes company-specific) collections of log macros, error handling macros, etc. The amount of boilerplate here is minimal compared to that.
It is, this is the most verbose way of doing it, it can easily be made smaller. The main reason is rust exposes this where other languages tend to hide it so programmers aren’t used to having so much code on error cases. Just my opinion of course
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.
If your error domain has only one useful category why not just create an error type with a useful message and be done with it. Why use anyhow at all? You are essentially saying the error domain is small so the work is tiny anyway.
anyhow seems useful in the very top layers where you basically just want bubble the useful errors modeled elsewhere to a top layer that can appropriately handle them. I don't think a crate should abdicate from modeling the error domain any more than they should abdicate from modeling the other types.
Yeah, I’m the same. I default to anyhow unless I need a strong API boundary (like if I’m publishing a library crate)
Sure, it’s slightly more error prone than proper enum errors, but it’s so much less friction, and much better than just doing panic (or unwrap) everywhere.
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.
Why are people disagreeing with this? This is absolutely a problem that most other languages don't have. If you want to claim that Rust's error system is "better" than anything else (as the author did), you should have a good argument about why this exact problem the parent commenter described, which to me is a major problem, does not (maybe) cancel out all the other purported benefits of Rust's error system!
> 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.
I’d rather have cargo than not. Dependencies are opt in you don’t have to use them, which is what I’m trying to demonstrate here. The chrome team only uses what they need. Now the culture as a whole in rust in always that way but I believe that to mostly be due to the newness of the lang and the quality of libraries
> Unless somehow these are compromised at the time of download, I will never have to worry about them again.
But this is exactly what rust does x) `cargo add some_crate` adds a line `crate_name = "1.2.3"` to your project config, downloading and pinning the dependency to that exact version. It will not change unless you specifically change it.
well, not quite. It'll go into the lockfile and you won't get a new version if you just build again, but if you add or remove a dependency that version may shift around a bit as a part of dependency resolution.
I suppose this largely depends on the kind of software that you write. Ideally, you also extract only the part of the external code that you need, audit it, and integrate it into your own code. This way you minimize the attack surface. I don't work on software that is exposed to the Internet however, so admittedly the importance of security vulnerabilities is low.
you could just do that with Rust, right? you’re just saying cargo makes it too easy not to
I’m very tempted to go this direction myself with Rust, vendoring in and “maintaining” (using Claude Code to maintain) dependencies. or writing subsets of the crates I need myself and using those. the sprawl with Rust dependencies is concerning
Yes of course you could do this in Rust. It's just that every resource out there promotes the usage of Cargo, and sells this as an "improvement" over the old school way of managing dependencies manually.
it's mainly an improvement because the rust ecosystem has a standardized way to build and distribute packages, so you can reliably add a dependency without build system pain. If you don't think having a reference to a central repo is a good way to go, you can vendor or even just pin your dependencies.
> 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:
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.
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.
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.
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.
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]`).
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].
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.
> 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.
Nodejs and rust are the languages that I’m most familiar. I mostly mean that part to serve as a contrasting paragraph between the two paradigms. The amount of code is high in rust, even higher due to me writing the most pedantic error possible. If you really want a more try catch approach you can do that with something like dyn error or anyhow. The point is it gives you choice
And it was treated as potentially malformed and hence the panic. That's what panic is for! When invariants are not upheld at runtime, in Cloudflare's case an abnormal amount of entries IIRC.
I mean, if the error was handled what would you have done if not crashing the service with an error message?
I think the post's point is that you don't panic if someone submits a malformed PDF (you just reject their request) but I don't think there's any way to gracefully handle a malformed config file that is core to the service.
>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.
It's really not fair to compare these when most of the errors of one language are caught at compile time by the other.
It reminds me of that scene from silicon valley "Anything related to errors sounds like your area
Can we not just agree that interpreted languages (save the Ackshually) like python and node need a more elaborate error handling system because they have more errors than compiled languages? It's not a holy war thing, I'm not on either side, in fact I use interpreted languages more than compiled languages, but it's just one of the very well-known trade-offs.
In the alternative, you would at least admit that error handling in an interpreted language is completely different than error handling in a compiled language.
>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.
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.
> 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` feature for that library isn't enabled. Nowadays, you can just implement the `core::error::Error` trait unconditionally.
As for backtrace functionality, that is on the cusp of being stabilized via a generic interface that allows `core::error::Error` to be defined in `core`: https://github.com/rust-lang/rust/issues/99301
> and it to this day has no built-in idea of "error accumulation".
The `Error` trait has always had this. It started with `Error::cause`. That was deprecated long ago because of an API bug and replaced with `Error::source`.
> It just felt like too much churn and each one offered barely any distinction to the previous.
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 `anyhow` and `thiserror` have been around for a number of years now and shows no signs of going away.)
> I don't bother with implementing `std::error::Error`, because it's pointless.
It's not. `std::error::Error` is what lets you provide an error chain. And soon it will be what you can extract a backtrace from.
> I'm kind of in the downcasting-is-a-code-smell camp anyways.
> 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 `anyhow` and `thiserror` have been around for a number of years now and shows no signs of going away.)
Thank you -- I just wanna say, I read a lot of your writing and I love your work. I'm not sure if I read that blog post so many years ago but it looks like a good overview that has aged well.
I still think the error chain abstraction should actually be a tree.
And I think they should never have stabilized an `std::error::Error` trait that was not in core. I think that itself was a mistake. And 8 years later we're only now maybe able to get there.
I actually said something on a github issue about this before rust 1.0 stabilization, and that it would cause an ecosystem split with embedded, and that this really should be fixed, but my comment was not well received, and obviously didn't have much impact. I'll see if I can find it, it's on github and I remember withoutboats responded to me.
Realistically the core team was under a lot of pressure to ship 1.0 and rust has been pretty successful -- I'm still using it for example, and a lot of embedded folks. But I do think I was right that it caused an ecosystem split with embedded and could have been avoided. 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.
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.
By error accumulation, I mean a tree of errors, not a simple chain. The chain is only useful at the very lowest level.
The tree allows you to say e.g. this function failed because n distinct preconditions failed, all of which are interesting, and might have lower level details. Or, I tried to do X which failed, and the fallback also failed. The error chain thing doesn’t capture either of these semantics properly.
Check out `rootcause` which is the first one I’ve seen to actually try to do this.
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 overstatement when what you actually mean is something more precise, narrow and niche.
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 reasonable people can disagree.
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`?
I'll list a number of things that I've experienced coworkers being confused about around the `std::error::Error` trait.
1) Why does it require `Display` and then not use it?
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?
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.
In a large project, most developers will be completely ignorant about the second more obscure possibility. And in most projects, you don't really need two ways to format an error. So I tend to do the friendliest thing for developers. There is only one way, and it is Display, which 100% of rust developers know about, and I avoid using `std::error::Error`.
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 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.
So funny how Rust advocates love to bash on Go but even something as trivial as error handling requires either a third party dependency (anyhow) that mimics Go's errors stdlib package, or they invent whatever this was...
The entire blog shows that you don’t need that… sushi showed the same thing in 1.0
Go dosent deviate from the norm. It’s the same style we’ve had back from the billion dollar mistake. Not saying it’s wrong rust’s is just different. Tradeoffs and such.
`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.
¯\_(°ペ)_/¯
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.
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.
anyhow seems useful in the very top layers where you basically just want bubble the useful errors modeled elsewhere to a top layer that can appropriately handle them. I don't think a crate should abdicate from modeling the error domain any more than they should abdicate from modeling the other types.
Sure, it’s slightly more error prone than proper enum errors, but it’s so much less friction, and much better than just doing panic (or unwrap) everywhere.
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.
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.
You can vendor deps with cargo if you want but fighting cmake/make/autoconf/configure/automake build spaghetti is not my idea of a good time.
But this is exactly what rust does x) `cargo add some_crate` adds a line `crate_name = "1.2.3"` to your project config, downloading and pinning the dependency to that exact version. It will not change unless you specifically change it.
I’m very tempted to go this direction myself with Rust, vendoring in and “maintaining” (using Claude Code to maintain) dependencies. or writing subsets of the crates I need myself and using those. the sprawl with Rust dependencies is concerning
I think you’re conflating the tool, with how people manage deps.
https://doc.rust-lang.org/cargo/commands/cargo-vendor.html
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:
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:
...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.
As it is any moderately large Rust project ends up including several different error handling crates.
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.
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.
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]`).
[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...
I mean I don't see the difference between having the non-exhaustive enum at the top level vs in a subordinate 'kind'.
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-...
"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.
I mean, if the error was handled what would you have done if not crashing the service with an error message?
I think the post's point is that you don't panic if someone submits a malformed PDF (you just reject their request) but I don't think there's any way to gracefully handle a malformed config file that is core to the service.
It's really not fair to compare these when most of the errors of one language are caught at compile time by the other.
It reminds me of that scene from silicon valley "Anything related to errors sounds like your area
https://youtu.be/oyVksFviJVE?si=NVq9xjd1uCnhZkPz&t=55
Can we not just agree that interpreted languages (save the Ackshually) like python and node need a more elaborate error handling system because they have more errors than compiled languages? It's not a holy war thing, I'm not on either side, in fact I use interpreted languages more than compiled languages, but it's just one of the very well-known trade-offs.
In the alternative, you would at least admit that error handling in an interpreted language is completely different than error handling in a compiled language.
Yes, that's precisely what I meant about "trade-offs that Rust clearly wanted to avoid".
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.
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.
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` feature for that library isn't enabled. Nowadays, you can just implement the `core::error::Error` trait unconditionally.
As for backtrace functionality, that is on the cusp of being stabilized via a generic interface that allows `core::error::Error` to be defined in `core`: https://github.com/rust-lang/rust/issues/99301
> and it to this day has no built-in idea of "error accumulation".
The `Error` trait has always had this. It started with `Error::cause`. That was deprecated long ago because of an API bug and replaced with `Error::source`.
> It just felt like too much churn and each one offered barely any distinction to the previous.
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 `anyhow` and `thiserror` have been around for a number of years now and shows no signs of going away.)
> I don't bother with implementing `std::error::Error`, because it's pointless.
It's not. `std::error::Error` is what lets you provide an error chain. And soon it will be what you can extract a backtrace from.
> I'm kind of in the downcasting-is-a-code-smell camp anyways.
I happily downcast in ripgrep: https://github.com/BurntSushi/ripgrep/blob/0a88cccd5188074de...
That also shows the utility of an error chain.
Thank you -- I just wanna say, I read a lot of your writing and I love your work. I'm not sure if I read that blog post so many years ago but it looks like a good overview that has aged well.
> I happily downcast in ripgrep: https://github.com/BurntSushi/ripgrep/blob/0a88cccd5188074de... > > That also shows the utility of an error chain.
Yeah, I mean, that looks pretty nice.
I still think the error chain abstraction should actually be a tree.
And I think they should never have stabilized an `std::error::Error` trait that was not in core. I think that itself was a mistake. And 8 years later we're only now maybe able to get there.
I actually said something on a github issue about this before rust 1.0 stabilization, and that it would cause an ecosystem split with embedded, and that this really should be fixed, but my comment was not well received, and obviously didn't have much impact. I'll see if I can find it, it's on github and I remember withoutboats responded to me.
Realistically the core team was under a lot of pressure to ship 1.0 and rust has been pretty successful -- I'm still using it for example, and a lot of embedded folks. But I do think I was right that it caused an ecosystem split with embedded and could have been avoided. 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.
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.
The tree allows you to say e.g. this function failed because n distinct preconditions failed, all of which are interesting, and might have lower level details. Or, I tried to do X which failed, and the fallback also failed. The error chain thing doesn’t capture either of these semantics properly.
Check out `rootcause` which is the first one I’ve seen to actually try to do this.
I’ll respond to the backtrace comments shortly.
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 overstatement when what you actually mean is something more precise, narrow and niche.
I do think that `std::error::Error` is mostly pointless. That's a value judgment, and reasonable people can disagree.
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`?
I'll list a number of things that I've experienced coworkers being confused about around the `std::error::Error` trait.
1) Why does it require `Display` and then not use it?
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?
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.
In a large project, most developers will be completely ignorant about the second more obscure possibility. And in most projects, you don't really need two ways to format an error. So I tend to do the friendliest thing for developers. There is only one way, and it is Display, which 100% of rust developers know about, and I avoid using `std::error::Error`.
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 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.
Go dosent deviate from the norm. It’s the same style we’ve had back from the billion dollar mistake. Not saying it’s wrong rust’s is just different. Tradeoffs and such.
if err != nil {