What do people love about Rust?
41 points by lonami
41 points by lonami
"if it compiles, it works" is real.
My first real rust project was a modular synthesizer (https://github.com/sharph/s-rack, if you are wondering) which involved concurrent UI and audio threads. It took me a couple days as a new rust developer to get a slider, an oscillator and sound output to pass the type and borrow checks and compile, but nearly the first time it compiled it ran as expected, which is amazing for a multi threaded audio app.
I really wanted to love Rust but that thing about how everyone uses anyhow to do basic error handling was such a turnoff for me. Is it really the idiomatic thing to use a third-party dependency to handle errors?
I've never tried anyhow and haven't had a problem using the built in error stuff in rust, but sometimes a library gets popular and that can be ok?
I actually think Rust does error handling pretty well. Errors are just (one constructor of) an algebraic datatype, and the language has ? + enough built-ins to make short-circuiting and error-handling ergonomic while reaping the benefits of having your errors just be data. (Some) people laud Go for making errors values, and Rust's treatment is in my opinion strictly better (but I've basically never written Go).
Unfortunately the type system is rigid in a way that makes it not ergonomic to "compose" multiple error types*, e.g. if you wanted to call a function which errors with FooError and then a function which errors with a BarError. This is where I have found anyhow to be useful.
But most of the code I write has just a single error type or I just brazenly put .unwrap() etc. in fallible functions (I mean yes, your file read could fail, but I am cool with my function just crashing with the file read error in that case; I do not write Professional Rust!).
Rust makes a lot more choices explicit compared to other programming languages, so don't be afraid to put .unwrap() or .clone() or whatever in your code to get it to work, especially at a first pass. You don't have to have perfectly "clean" and "Rusty" code, whatever that means.
* I did some digging and this also happens in Haskell too; again I don't write good code, so of course never ran into these issues myself
I really love Rust’s unopinionated take on error handling. It has let the ecosystem thrive by letting people explore the space of possible error handling solutions. Of which there have been… quite a few. Thiserror, anyhow, eyre, color-eyre, tracing-error, error-chain, failure, just to name a couple.
Each of these libraries has brought good ideas: thiserror for lightweight, specific errors; anyhow for easy catch-all and contexts; eyre for stack trace filtering, color-eyre for coloured output, tracing-error for spantrace (vs. stacktrace) errors, etc.
What’s unfortunate in all of this is:
My dream is for an error library to seamlessly merge all these approaches (without falling to the XKCD “N => N+1 universal formats” problem), and then on top of that add the brilliant (if underappreciated) approach from the komora-io/terrors library.
That library abuses the Rust type system to twist it into providing ergonomic error handling without loss of specificity. For example, letting you declare that a function returns io::Error + OtherError without having to define a new sum type. Then the caller can handle just one of the errors and itself return the remaining one. I believe it provides the ergonomics we are missing in Rust.
Two things that would make error handling better in Rust are
These two things would allow for functions to specify that they can only ever fail in one way out of the many in a larger Error enum, and on the other direction to be able to avoid having to declare large enums to begin with.
For libraries, you would still end up with things like what you have today, but for applications or exploring the API you're building, these would help.
I believe anyhow's value-add is context which is always ferociously hard in error handling situations
I believe it is Rust's answer to fmt.Errorf("I was trying to ... when %w", err) producing the much nicer I was trying to ... when stat config.ini no such file or directory
Having written out this comment, maybe your observation is that Golang ships with fmt.Errorf and Rust makes one pull in a random dep from crates? If so, I'm firmly in your camp because I'd much rather be stranded on a dessert island with the Golang standard library than to have to contact crates to have access to regex functions in 2025. But, I guess the Rust folks follow C's mental model of "you get nothing, good day, sir"
I don't know Go, but why couldn't you just do
Err(format!("I was trying to ... when {}", err)) // type Result<T, String>
Is the Error type in Go something more sophisticated than String?
Anyhow, I'm not a Rust expert but when I've seen anyhow used in codebases I've worked on, it was for dealing with cases where there is no singular error type for e.g. a function.
fn example() -> anyhow::Result<()> {
std::fs::read(...)?; // Result<_, std::fs::Error>
my_fallible()?; // Result<_, MyError>
}
But maybe that is overkill and Box<dyn std::error::Error> works just as fine, as mentioned in their docs. I haven't tried it personally (it seems like it would be annoying to have to add the wrappers).
It is.
The %w preserves the other error type so you can use 'errors.Is()' on it. https://pkg.go.dev/errors#Is
Go has an “error wrapping” system that lets an error be a tree of “wrapped” errors. The optional Is and As methods let you use that tree as a sort of simple type system with inheritance. This was a fairly late addition and has nothing to do with the actual type system; it’s done with typecasting and reflection to look for the Is and As methods dynamically.
The error type in Go is just an interface that lets you convert an error to a string. There’s no structural requirement on errors.
My perception is that Rust tries to minimize the standard library- they only put there things that would be a problem if they were outside the standard library.
There are simple error handling patterns for applications, such as boxing errors that require no third-party library, or just even choosing to panic on errors (which IMHO is fine for a lot of programs!).
Anyhow adds opinionated error handling for programs; opinionated meaning that it's a matter of taste- and a lot of people happen to like it! So although you don't really need libraries to do proper error handling, you can use a library for extra functionality.
(The two main error libraries, Anyhow and thiserror do not appear in your public API; Anyhow because it's not meant to be used in things that provide a public API, and thiserror, which is intended for things that provide a public API, deliberately avoids it. So actually, people using those error libraries do not "force" anyone to use them- which IMHO means it is correct for nothing like them to be in the standard library.)
My perception is that Rust tries to minimize the standard library- they only put there things that would be a problem if they were outside the standard library.
I have an awful time using anything but the most mainstream of crates though.
But I guess it's fair because the solution space that Rust has to cover is much wider so it doesn't make sense to ship with too many assumptions in the stdlib.
Well, they already make some allowances with core < alloc < std. All three of them could be smaller, however, I don't think you could remove things from them without causing issues.
My uneducated perception is that, for example Vec doesn't really need to be in the standard library. However, many libraries use Vec in their APIs, so if there was no standard library Vec, you'd likely have the fun of having to deal with multiple independent implementations in your code.
You might already know https://blessed.rs/crates; it's my first stop when I need a crate. Normally what I find there is usable enough (in relation to the complexity of what you want to do with it- macro helpers are never really going to be "easy", I guess).
for example Vec doesn't really need to be in the standard library
Maybe that's true but it would be jarring similar like how Gleam doesn't ship with file IO.
blessed
I forget about it, but yes, that is an essential resource if you want to maintain a small stdlib.
macro helpers
It is my opinion that macros are bad bad bad.
It's idiomatic to use libraries for everything that doesn't have to be tied to the compiler internals.
You have the ? operator and some traits/types that need to be hardcoded to work out of the box across the ecosystem, but everything else can be implemented however you want. Implementing it anyhow's way is one option, but you can implement it differently if you don't like the particular combo of heap allocations and type erasure.
I see this as natural progression of unbundling everything from the language.
We used to have the opposite extreme where BASIC was the OS, IDE, and there was no concept of libraries or extensibility at all. There were languages unbundled from the OS, but still were their own IDE, with everything built in. IDEs got unbundled into separate compilers and support for 3rd party libraries, but still tried to have everything built-in.
The next step is to unbundle all built-in libraries from the compiler, making it completely universal and treat all libraries the same (Rust isn't there yet, but is trying).
Many languages we still use today are older than the Web, and were developed when disk space was scarce, so they treat libraries as a special case of 3rd party extensibility done with great pain as last resort, rather than as architectural separation of library code from the language.
If you come from a pre-Web language, go to https://lib.rs/std and pretend you already have all of these libraries bundled with the language.
I would really rather have exceptions than use Result/Option absolutely everywhere. It's not because I hate types. I'm an OCaml programmer, I love types. But even in OCaml we use exceptions from time to time.
The ? operator is the sweet spot for me. It makes syntax minimal and unobtrusive like exceptions (none of the if err != nil noise) while remaining in the realm of normal types and keeping control flow independent of error reporting.
try/catch has a fatal flow of mixing error reporting with control flow, so it can only report one error at a time, interrupting whatever has been happening. With Result the same mechanism can defer/delay, stack and collect errors using the same data flow as the rest of the programming language.
I like anyhow when writing a cli application. I would not use it in a library. With such a distinction, i consider it ok that it isn’t in std.
This is what the answer was when I asked about it: https://lobste.rs/s/irzh59/why_i_don_t_love_rust_either_2021#c_qllhll
I still think it's bad and anyhow is poorly documented.
The argument that it not being in included could generate better things hasn't really paid off until I recently saw this which I'm eager to try out (really, anything rather than anyhow): https://github.com/shepmaster/snafu
I think what you're missing is that the recommendation is Anyhow and thiserror! Snafu looks closer to thiserror.
Anyhow is for applications, thiserror is for libraries.
(And in many cases, an application is an application and a library!)
Notably this later on talks about downsides and recommendations to remove those in the future.
I've had the opposite "it compiles, it works" experience with unsafe Rust code. I write unsafe code that looks right, compiles without complaint, and then at runtime immediately segfaults. Mostly when passing memory over FFI to C code and I forget to explicitly tell Rust to not drop values that get passed to C. Also some code seems to work and then a week later I realize its unsound in some edge case. Ergonomics and tooling for unsafe Rust could really use some improvement.
Also theres some stuff one still cannot do from Rust, such as invoke vfork or setjmp. Yes, I know these are bad in the general case, but I own code that has valid reasons to use vfork (actually clone3 with the right flags) but that could would not be directly portable to Rust.
The most obvious case of constructing a droppable type and then taking a pointer to it in the same expression now has a lint, but other cases no so much. If you have cases that aren't covered by this, it'd be interesting to see if there's any way of having best effort detection for them. I'm the end, sadly, when dealing with unsafe you have to rely on runtime analysis like miri or *san.
https://doc.rust-lang.org/beta/nightly-rustc/rustc_lint/dangling/index.html
For setjmp, you might want to take a look at https://github.com/pnkfelix/cee-scape, which I don't think was ever productionalized as there are some additional preconditions you need to enforce (like only dealing with PoD), but might be a good starting point for a safer abstraction you can use.
I'm really rolling the dice weighing into this thread, especially with this observation, but I mean it sincerely so here we go:
Finally, one of Rust's most important virtues is its extensibility
I detest with all my heart languages that secretly import symbols, and have avoided Scala for decades because of that nonsense. I recently learned that Rust seems to subscribe to this behavior, too
// use hmac::Mac;
fn example() {
let hmac_key = [0u8; 32];
hmac::Hmac::<sha2::Sha256>::new_from_slice(&hmac_key).expect("bogus hmac init");
}
does not compile, citing no such symbol new_from_slice but uncomment that use statement, observe that the symbol Mac doesn't appear a single time in that fn and be sad
Isn't that because new_from_slice is a trait implementation? So once you import Mac you can access the trait implementations and therefore the new_from_slice function?
Yes. It is, in fact, not secretly importing the trait for you. You have to do that on your own, or it's not available.
but when a normal person sees use foo::Bar; how in the world are they supposed to know what symbols it magically imports, because it for damn sure doesn't import Bar anywhere. It seems like it is almost doing use foo::Bar::* and then it is left as an exercise to the reader to know what symbols are in * and (in this specific case) to which other types those symbols are attached
Look, I have a copy of RustRider and it is great for command-clicking into all kinds of things, however, until JetBrains unfucks Qodana to make it usable on GitHub then I'm left trying to eyeball PRs or check them out locally like a caveperson in order to know what it does. Every magical symbol places the burden upon me to guess where it came from and what it does
I’m not quite understanding the alternative. The Mac trait is being imported — Mac is certainly in scope if you want to use it, say to define another impl of it. That trait is what defines new_from_slice (and 12 other methods) in the first place.
Do you want to have to import every trait method separately? (Does that mean you want to import every struct member separately as well?) Or do you want to have to explicitly name any trait when you use it? (That would get super tedious.)
I guess what's happening here is "well, my favorite color is ..." and in that way I'm just wasting both of our times since it's clear we have different views on API discoverability. However, because this thread is thus far polite and you did ask a reasonable question, I'll do my best to answer it. I should also qualify my perspective that I've only been "weekend dabbling" in Rust, and thus if one acquires Stockholm Syndrome after years of this, that could also easily explain our different perspectives
I think your perspective aligns with the Scala and Lombok community of "do all the magick because the ends justify the means" and my perspective is that code is designed to be read, otherwise we'd be writing in assembly
For example, Kotlin also has extension methods that can be post-facto declared on any type in scope, but in order to do so one must request that outcome https://kotlinlang.org/docs/extensions.html#scope-of-extensions So, based on your question, yes, it actually would be far better to have any symbol that artificially arrived in scope to be declared. It's not the catastrophic outcome you make it out to be since rust already has wildcard imports (just like Scala and Kotlin) so if someone wanted to save everyone the typing, sure, ::* to your heart's content. That would not help my specific observation about "where did this symbol come from?" in PRs. But that's a project style concern versus -- as best I can tell with Rust's approach -- there is literally no remediation except annotating the use site with // here's why comments like folks did back in the #include days for the exact same reason I'm calling out. To take this to the very extreme, under the "literally import the symbols" scenario, there's nothing stopping an autoformatter from expanding the ::* into the consumed declarations, so one need not do them by hand, either
"do all the magick because the ends justify the means"
The "magic", if you want to call it that, doesn't really have anything to do with imports though. Just like how you don't need to import the methods and fields of a type, you don't have to import the methods of a trait. The magic is in the compiler inferring which types and traits you're using. If you want to be explicit about types and traits, there is syntax for that too, of course:
use hmac::Mac;
fn example() {
let hmac_key = [0u8; 32];
// option 1, fully specify type and trait
<hmac::Hmac::<sha2::Sha256> as Mac>::new_from_slice(&hmac_key).expect("bogus hmac init");
// option 2, specify trait, let the compiler infer the type from the destination of the expression
let a: hmac::Hmac::<sha2::Sha256> = Mac::new_from_slice(&hmac_key).expect("bogus");
}
I think I do understand and empathize with your misgivings about these sort of "ambient" effects of use. As another example, I do a lot of embedded Rust, where something like use panic_halt as _ sets the panic behavior, without really looking like it says that.
I would say, though, that extension methods aren't very comparable to traits, and if you accept traits as useful, there may not be a better way to handle this "ambient" aspect of them. (If you think traits are themselves too "magical" then I think we'll have to conclude Rust simply isn't to your taste, which is fine!)
Extensions are inherently independent functions, so it makes good practical sense to import only one of them. A trait, however, is more like a struct: a set of things that are often used together. So it makes more sense to import the trait (or struct) as a unit rather than its individual parts. While you might be able to come up with a "partial Import" for trait members (though that sounds like it might be quite a rabbit hole RFC), I suspect if you tried to apply it to actual code, you'd end up using * most of the time anyway.
Kotlin is an object-centric language, so extensions are on some particular receiver type. But traits can define things that aren't "methods". In this case, new_from_slice is a function where the trait is associated with the return value, not the receiver (there is no receiver). They don't even have to be on concrete types — the new_from_slice being used here is in a trait that is a convenience wrapper around the completely different new_from_slice in the KeyInit trait that is actually implemented by the concrete types.
BTW, the idiomatic way to say "I want the trait implementations but I'm not using the trait directly" is
use hmac::Mac as _;
so no comment is required once you learn that.
I'm not really sure how to fix that; but it is highly annoying over the course of the first couple hundred hours of writing Rust.
I'm not really sure it needs to be "fixed". I think the diagnostic messages emitted by the compiler pretty much tell you which trait you haven't imported when you build, and when you're using rust-analyser as an LSP, I believe it provides guidance on available trait methods one could import. I think otherwise it's just an intended part of how traits work?
That's fair, though even with rust-analyzer it feels like it doesn't always correctly import them automatically. It does at least provide all the methods that importable traits provide though. It just feels like a rough little edge to write the function call from the LSP auto-complete, hit compile, and then get presented an error, even though the error is trivial to fix.
Some part of the post go in this direction but I’d like to see a reciprocal post “What do people hate about Rust” with deep dives into controversial topics! There is a bunch of them IMO. For instance, try to write and implement an async trait, i.e. a trait with some async functions. I never managed to do it properly without the crate async_trait, it’s really difficult.
And maybe it’s just me but I still don’t quite understand some details behind pin and unpin. Pinning is often necessary with async code.
My experience with writing Rust software tends to be once you've got it working, it stays working.
Somehow, this phrase made me giggle.
"if it compiles, it works"
Yeah, good luck getting anything non-trivial to compile. I usually have to go into a project's Discord to figure out compile errors that the LLM can't help me with.
When encountering cases like these it would be appreciated to receive bug reports against the Rust tooling. At the very least rustc and cargo errors should be clear on what the problem is, preferably actionable, and ideally made to work without an error. In many cases the problems are caused by bad or weird interactions between projects and user environments that are hard to be identified ahead of time, but once the project is made aware of them the tooling can be made to account for them.
I routinely have large troubles compiling non-trivial crates requiring me to similarly beg for the incantation required to build everything… but it’s exclusively limited to crates that call out to C/C++. Rust-only crates work great for me.
For your own projects, the more you use the language the quicker you are to spot bad patterns that don’t compile. Same is true for every language.
It's always C and C++ bindings. Not really Rusts fault that not everything was rewritten to a rust.
Using nix and nic dev shells helps a lot.