Hacker News new | ask | show | jobs
by osiris88 175 days ago
> 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 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.

1 comments

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.

Let's continue discussion here: https://news.ycombinator.com/item?id=46416377

I think it's true that right up until 1.0, backtrace was a part of `trait Error`, then it was deprecated and removed, but there were ongoing discussions as late as 2021 about how the "point" of `std::backtrace::Backtrace` was to attach it to an `Error`. I have lots of links and references there.

As a user, that kind of meant that as long as I thought `trait Error` might grow backtraces someday, I should stay away from it in order to be friendly to embedded. And as long as it wasn't in core, that could still happen. I hope you can agree that it was far from clear what was going to happen until relatively recently.

> 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.

Yeah, the edition thing is cool, but there's also a cost to it, which is that over time you accumulate more and more support code in the compiler for really ancient editions.

I don't have a super good understanding of how it actually works in rustc. In C++ typically they would use name mangling tricks to try to keep new and old standard library symbols from clashing if they decided to break compatibility. Probably with rustc they are happier to leave it unspecified. I have a hard time building a mental model of what kinds of things would be totally impractical to change with an edition.

For the Range types, what would happen is (AIUI) something like this (example is for Range, but several other types are affected similarly):

1. The new ranges are stabilized, they become usable from stable Rust, as well as a core::ops::Range, the half-open range type in today's Rust, you'd be able to make a core::range::Range, a modern rendition of the same idea. The new type implements Copy and IntoInterator, and provides an iter() method to iterate over immutable references, all things the old type wasn't designed to accommodate.

2. A new Rust edition says that now A..B is syntax sugar for core::range::Range { start: A, end: B } rather than the same idea but for core::ops::Range. In your existing Rust your range notations are thus still core::ops::Range, but in new Rust projects written for a new edition they are core::range::Range with the nicer semantics.

So, there's no name mangling trick, the types continue to both exist, just when you write 0..4 meaning the integers 0, 1, 2 and 3, now you mean the modern type. You may need to write a little glue for older libraries to turn your modern types into Iterators they expect, but it's trivial and soon enough all modern Rust code would behave as though the ranges had always implemented Copy and IntoIterator just as today Rust code behaves as though the arrays had nice properties which in say 2018 Edition they did not.

In terms of mental models, an Edition can change what the concrete syntax of Rust means, so often the strategy is in two parts, 1: In ordinary releases ship an awkward way to express the new idea, this is 100% compatible with existing code, but has poor ergonomics - then 2: In an Edition change the language so that the new idea is easy to express

So you can see for Ranges, that first element would be stabilizing core::range::Range (and other similar types) then the edition would change the syntax.