Ironically the "simple" JS program has a bug in it. The documentation for fs.watch is very explicit that the filename in the callback can be null and that you need to check for that. In Rust that fact would be encoded in the type system and the programmer would be forced to handle it, but in JS it's easier to just write bad code.
Typescript would require you to check for null before use, which I think makes this a good example of how TS is oftentimes a fairly gentle step up from JS. Or, at least, closer to the correctness of Rust without all the heavy stuff.
You manually manage memory via ownership rules and lifetimes.
You can allocate memory for a Vec with Vec::new. Dropping it deallocates. When and where it's dropped is important. Passing ownership around is important. These are parts of managing the memory.
If you need something to be on the heap, you use Box. If you need something to be shared, you use something like Arc or Rc.
Rust makes it nice to implicitly handle the common cases so you don't feel like you're doing manual memory management, but you're making decisions about that memory management at every step of the way.
To add to this, Java and other GC languages in some sense have manual memory management too, no matter how much we like to pretend otherwise.
It's easy to fall into a trap where your Banana class becomes a GorillaHoldingTheBananaAndTheEntireJungle class(to borrow a phrase from Joe Armstrong), and nothing ever gets freed because everything is always referenced by something else.
Not to mention the dark arts of avoiding long GC pauses etc.
It's possible to do this in rust too, I suppose. The clearest difference is that in rust these things are explicit rather than implicit. To do this in rust you'd have to use 'static, etc. The other distinction is compile-time versus runtime, of course.
> The clearest difference is that in rust these things are explicit rather than implicit. To do this in rust you'd have to use 'static, etc.
You could use 'static, or you can move (partial) ownership of an object into itself with Rc/Arc and locking, causing the underlying counter to never return to 0. It's still very possible to accidentally hold on to a forest.
> It's easy to fall into a trap where your Banana class becomes a GorillaHoldingTheBananaAndTheEntireJungle class(to borrow a phrase from Joe Armstrong), and nothing ever gets freed because everything is always referenced by something else.
Can you elaborate on this? I'm struggling to picture a situation in which I have a gorilla I'm currently using, but keeping the banana it's holding and the jungle it's in alive is a bad thing.
The joke is you're using the banana but you didn't actually want the gorilla, much less the whole jungle. E.g. you might have an object that represents the single database row you're doing something with, but it's keeping alive a big result set and a connection handle and a transaction. The same thing happening with just an in-memory datastructure (e.g. you computed some big tree structure to compute the result you need) is less bad, but it can still impact your memory usage quite a lot.
To extend upon this, memory generally has a single owner. When it goes out of scope, it gets freed [1]. The drop() function, which appears analogous to free() in C/C++, is actually just an empty function who's sole purpose is to take ownership and make it go out of scope, which immediately frees the memory [2].
> This function is not magic; it is literally defined as: pub fn drop<T>(_x: T) {}
This is usually more deterministic than GC languages (no random pauses), but can be less efficient for highly nested data structures. It also makes linked lists impossible without using "unsafe rust", as it doesn't abide by the normal ownership rules.
Linked lists to arbitrary memory, yes. Linked list from a consecutive chunk of memory managed by a bump allocator: just as easy as any language, no need for unsafe.
Admittedly not the easiest language to make a linked list in.
The way I sometimes describe it is that memory is essentially managed at compile time in Rust, whereas with a (tracing) GC it happens at runtime, and in C it's done manually by the programmer. This is a simplification, but it's both true that Rust is similar to C in that there isn't a runtime cost to memory managed and that it's similar to Java in that outside of specific APIs, you have strong guarantees about certain issues with memory not being possible because they're automatically handled by the language.
I half-heartedly tried to make "static memory management" (Rust/C++ RAII) vs "dynamic memory management" (GCs or Rc/Arc in Rust/C++) happen but people generally didn't like it. I do think it's a good framing though.
Yes, by deciding on when, where and how you pass it. You know when it is created and destroyed because you wrote the code that either does that directly, or follows the convention (destroy when out of scope, or use of arc - it is programmer's decision).
False set of options, people just haven't been using the right systems languages with automatic resource management, that also offer escape hatchs to manual memory management.
Cedar being one of the very first ones.
Whose ideas perculated to Modula-2+, Modula-3, Oberon, Oberon-2, Component Pascal, Active Oberon, D, C# (after Midori learnings finally found their way into the language), Swift, Chapel, and whatever comes next where researchers now try to mix both productivity with modern type systems.
JS will immediately error on the lack of parens, but the `in` vs `of` iterates over indexes, not values, and those indexes are unfortunately converted to strings (since `for-in` is object field-name iteration). So even TypeScript would not have caught it when the (stringified) index is used as the first argument of `fs.watch()`.
True, but also the loop syntax is simply incorrect, which would have been caught by running it; so probably a better interpretation is simply that the author didn't spend much time thinking about that JavaScript code because it didn't matter for their point.
But isn’t it at least part of the problem? The JavaScript code looks completely fine, until the HN comment section finds various subtle bugs that sometimes not even TS can protect you against?
no. the JS program has obvious syntax bugs that TS wouldn't compile. commentors above are right that I was just lazy (ty for the callout, I have since fixed the JS program).
println can only print things that implement the traits Display or Debug. As a result, Paths cannot be printed directly.
Not all OSes store paths compatible with UTF-8. In Rust, all strings are UTF-8 encoded. Thus printing a Path is a lossy operation (i.e. cannot safely be round-tripped). Path exposes a `display` method returning a type that implements Display. This is a fact Rust is encoding in its type system, whereas in JavaScript (and TypeScript), it's not really possible to state "strings internally are UTF-16 and you may need to invoke TextEncoder / TextDecoder to safely deal with paths that are not Unicode". If you fetch from a server sending Shift_JIS text and invoke `response.text()`, you get an empty string at runtime (in my experience). If you aren't experienced in dealing with text encoding issues, I can see this becoming a lengthy debugging session.
As others have noted, the JavaScript program has a bug not present in the Rust program, and a syntax error (should probably use for..of, not for..in). The example definitely uses more concepts than "first-class functions". You still have to understand iterators, just like in Rust, and it uses CommonJS instead of ES Modules, but I digress. The usage of async/await and Promises is another concept to teach, and the usage of top-level await is specifically something that wasn't supported in some runtimes (e.g. Node) until recently. It still isn't supported in the latest version of some mainstream JS engines (e.g. Hermes, which is used in React Native)
To your above point about paths, it’s things like this that keep me coming back to Rust. This was just one example, but other languages are absolutely littered with pitfalls and gotchas that—individually—aren’t all that likely to happen.
But all of them? Over a given program’s lifetime? In aggregate these end up being responsible for an absolutely wild number of random bugs that crop up in your exception logs that need to get tracked down. And it never ends. There are an infinite number of edge cases you’ve never thought about that aren’t fully handled and that some of which are constantly being tripped.
This just… doesn’t happen in Rust. The type system catches (and/or completely rules out) a ridiculous number of these situations. I have repeatedly written finished software in Rust, where once it was released there was only an occasional need to add features but the typical bug-squashing treadmill just doesn’t exist.
That’s not to say there are never bugs. You can express faulty logic in any language. But the number of stupid impedance matches that are categorically ruled out makes the experience of operating and maintaining Rust program once written a widely different experience than I’ve had with any other language.
I spent months working out all the kinks of a C++ codebase to make up for decades of assumptions about paths and strings and locales that failed to stand up to reality.
The 'difficulty' of Rust would have been preferable, in hindsight.
I find that understanding thenables/Promises and async-await in JS/TS is emphatically not something a lot of devs really understand. It really shows when you see something like...
var fn = async (param) => new Promise((res, rej) => {
...
fooLibraryCall(param).then(res).catch(rej);
});
I've literally seen similar to this... where devs see wrappers for the callback syntax, then apply the same to thenables and use manual Promises inside an async labelled function. It makes my mind bleed to see it, and I've seen it a few places.
Then, of course, there's dealing with module imports and async import() methods and how they operate or get transpiled or split, etc.
Sometimes you still need to work with promises even inside an async context. Imagine an async function which needs to `await Promise.all([...])`. In this case, `then()` can be a useful tool to make new promises from an async call.
Of course.. I was referring to having an async function, but having it create a "new Promise" that in and of itself is just calling a method that already returns a promise... it's creating extra closures and overhead that aren't necessary at all.
Sometimes I'll add a `.catch(() => null)` or similar for manipulating a specific call with `.then`, if I want to manipulate something out of flow. Such as a convenience method to normalize api fetch calls, etc. That's just useful functionality, not making literally wrappers for no benefit or reason.
The Bjarne quote is basically sales pitch for a recurring rationale to make C++ worse and worse. It was, I suppose, not unreasonable to assume Bjarne was sincere the first time, but that was a long time ago. Here's how it goes:
1. “Within C++, there is a much smaller and cleaner language struggling to get out”
2. However just subsetting the language to get at the smaller one would not be a cleaner language. Instead we must first make a superset language, adding features, then we can subset this new language to reach our smaller but cleaner C++
3. Step one, superset will land in C++ N+1. Planning of that "subset of a superset" will need to wait until we've completed that work.
4. C++ N+1 is an even clunkier behemoth. Rinse and repeat.
I don't understand why people who've seen this happen more than once would stick around. You're not going to get the "smaller and cleaner" language after step two, there is no step two, it's just going to be step one again and then step one again, and then step one again, forever.
reminds me of the classic https://xkcd.com/927/ not exactly identical to Bjarne's quote but similar.
I'm quite familiar with C++ as well and this just jives so much, each standard is just almost exponentially more complicated than the last, and while there are good changes they don't necessarily fit well with the prior version and its just a mess, I still maintain two OSS libs but I don't use the language anymore.. so its a question of how long I put up with it at this point.
Rust is such a breathe of fresh air coming from c++11/14/17/20 but its still a behemoth if you don't know the entire thing, I think this article is pretty spot on with that.
Anyone else get completely side tracked as soon as you saw the shebang (self-executing rust scripts)? MY mind kind of exploded in a similar fashion to when I discovered Go could do the same. It's definitely a nifty feature and can see it getting a lot of basic usage. I've seen a couple projects that do similar with rust to control build and testing pipelines, this could be a good alternative in those cases.
That said, I mostly just use Deno + TS for my shell script needs beyond simple bash. Mostly in that JS is hands down the language I know the best (28 years), close would be C# (24 years) for me. I was also an early adopter of Node. I also think that dealing with shared/centralized packages is an easier option for Deno than Node, Python or other languages/environments. The cargo front-matter here seems to work similarly.
As the person designing and implementing cargo script integration into cargo (there have been many third-party implementations in the past), I was both glad to see it in the wild and surprised and glad to see it called out like this!
Yes, there has been a long road to this in defining what this should look like, how it should interact with the language, what is the right scope for the initial release, and so on.
At this point, I'm doing what I hope is the wrap up work, including updating the style guide and the Rust reference. The big remaining work is in details I'm working out still for rustfmt and rust-analyzer. Other than those, I need to get to a bug fix in rustc and improve error reporting in Cargo.
For myself, I use cargo script nearly daily as I write a script whenever I'm creating a reproduction case for an Issue I'm interacting with.
Just to add, I literally meant sidetracked... started searching for the "-Zscript" feature in cargo, apparently in progress since 2023 with an open issue that's close to complete. Along with looking back into ZomboDB's repo, where I saw Rust being used for the build pipeline stuff, not that I completely understand it in context.
Not to mention how useful the cargo front-matter handling is in terms of portability for these kinds of scripts... one file to share, and no extra "install/init" step to bring in dependencies (such as with say Python or Node.js).
I saw an example about a year ago and can't find a specific article... but something along the lines of...
#!/usr/bin/env -S go run
I'm not sure if/when go ignores a shebang on the first line... otherwise, there are a bunch of articles I've seen that have a slightly differing format to do the same.. but google results feel off/old.
searched for: go shebang scripts
shebang is the term for a text/script file beginning with "#!" with the executable and parameters to run.
> That said, I mostly just use Deno + TS for my shell script needs beyond simple bash. Mostly in that JS is hands down the language I know the best (28 years), close would be C# (24 years) for me
I sympathise, but in 2025 that is a terrible reason to choose a system.
A lot has changed, mostly for the better, much better, in the last twenty years
I know it, understand it, it's easy for me to use, modern, can run without complex initialization/setup, a massive amount of module support including plugging into almost every kind of backend under the sun, and portable as a single file/script.
Those aren't good reasons to choose something?
I mean, sure, I COULD use Python or Go, or actually build small programs in a number of other languages instead of scripts altogether... But Deno+TS is actually a pretty good and capable shell scripting option. I'm not bashing on Python or Go here.. but given that I KNOW one of these options better than the others, it definitely is more than enough to tip the scales.
Unless you're making the assumption that my starting with JS so long ago means I don't understand modern/current conventions?
Your choices are modern. Not sure what the person arguing exactly about, but new C# is as modern as it can be, and .ts is pretty damn modern as well. Same for deno in 2025.
In fact, in 2025 I tested same setup for better scripts: deno+ts.
I know .ts pretty ok, but the pattern didn't stick to me with deno, especially when Python received uv.
But both options are fine. Saying that in 2025 you should so something different and more modern and not elaborating makes 0 of an argument.
PS C#/.NET just introduced "run", won't be much sense to use deno+ts once it gets up to speed as native integration.
I know this... my point was in that Rust didn't have this feature 5-8 years ago when I first started reading/learning about it. A lot of languages haven't had an active scripting-capable usage that can do this until fairly recently. C# even has it in .Net 10, though I'm not sure about dependency handling in that case.
I like and was commenting that rust has the cargo frontmatter instead of a separate cargo file, same for deno in that it can reference the repo/url directly instead of requiring a separate modules file/folder (like node package.json/node_modules) or even with python. You can reference the modules you need directly.
I remember when I saw this in F# interactive (C# didn't have a REPL at all at the time) when I first started using that language around 10 years ago. I thought it was immediately useful as a easy to write, but static typed Python replacement without all of Pythons dependency headaches for quick scripts. Because back then as compared to say C#/Java which required a lot of class files, boilerplate, etc F# read more like a scripting language which made it more suited to this. When they added the ability to pull in packages/dependencies inside the script directly that was great as well. e.g. Pull in a type provider, parse some JSON, make some API calls, create quick CSV's, whatever. Useful for quick scripts, data mangling or quick prototypes - you could just cut and paste the single script with a shebang, message it to someone on your team and it would run dependencies and all without the equivalent venv issues with a simple pasting into a text file. Install .NET SDK and that's it - it would run. In the team I was in at the time it was a quick productivity boost - seeing features implemented in quick scripts and seeing the data workable in a REPL session which could then could be promoted to an actual project was a great workflow.
I think those features have since spread to C# interactive (probably inspired by the F# incubator IMO), as that language has done things to simplify its syntax to make single file apps easier. From your comment Rust seems to be taking a similar approach.
A bunch of third-party Python package manager clients, most famously uv, support this now. They all use the same front-matter format (PEP 723). Unfortunately, pip (the first-party package manager client that's usually distributed with Python) doesn't support it yet: https://github.com/pypa/pip/issues/12891
Interesting... I'm not really involved in much/any Python development, but cool to know there are options out there. I've mostly just run existing projects as needed.
To be precise, Rust files have supported shebangs since well before 1.0, there just wasn't really any built-in tool to point the shebang at. The new stuff in the relevant RFC is actually about Cargo, not Rust; the "Cargo frontmatter" syntax doesn't need to be added to Rust, it can just be stripped by Cargo before it hands off the file to rustc.
Most languages currently do not support this in a way that lets you specify dependencies from a package manager. And some don't support it at all; most C and C++ toolchains, for example, don't have a way to compile and run a single source file in a single command.
Exactly, that alone makes it far more useful/portable in practice than a lot of other languages for this type of thing. A python file can have a shebang, but it'll need a bunch of other files around it for module references, etc. Not to mention initialization/setup, etc.
The fact that dependencies are referenced in the file and automatically handled at runtime (first or repeated) is a really nice feature to have... it's expressly why I started using Deno over other options for this.
I would argue that third-party tools don't really cut it, because a lot of the value is being able to include a script inline in, e.g., an email or chat message, and that's undermined if the recipient has to download and install a separate tool. (uv gets half credit because adoption is rapidly rising and it has a shot at becoming a de facto standard, but I'll only award full credit to Python when pip supports this.) They're good for exploring the design space, though.
So what exactly is the "much smaller and cleaner language struggling to get out" of Rust? If I'm understanding the post right, that language still has references, lifetimes, traits, enums, etc., because all of those features cohere; you can't remove just one and expect the rest of the language to still work. Once you grant all those features, your language isn't much smaller or cleaner than Rust; your language pretty much is Rust.
The last section gives two different hints as to what this "smaller and cleaner" language might be, but neither of them fully makes sense to me.
First, withoutboats's "Notes on a smaller Rust". That post and especially its sequel are great and I like them a lot, but the title is fairly misleading as to what they're getting at. The language that boats sketches out in those posts has significantly different design goals from Rust; in particular, it abandons the requirement of low-level programmer control over runtime behavior, and so is unsuitable for many use cases that Rust is used for. The idea, rather, is to explore what lessons Rust can offer for the design of a language with more "mainstream" requirements (i.e., one that can afford things like a tracing garbage collector, and wants to avoid Rust's downsides compared to other popular languages, like slow compile times and heavy syntactic salt). That language is not "struggling to get out" of Rust; Rust doesn't want to be it.
Second, "In a manner of speaking, that smaller Rust is the language I fell in love with when I first learned it in 2018. Rust is a lot bigger today, in many ways, and the smaller Rust is just a nostalgic rose-tinted memory." I've explained above why I don't think boats's proposed "smaller Rust" is anything like the real Rust was at any point in its history (at least after the very early days, once the designers figured out that they were targeting C++'s niche). In most fundamental respects, Rust hasn't changed that much since 2018, and a lot of the changes (like the new editions) are about making it more syntactically flexible and increasing the fraction of sensical programs that compile. That said, there are two big exceptions: async and const, which were much more minimal in 2018 and have since expanded to big complex meta-features with many interlocking parts that weren't part of the language's original core. If the claim is specifically that Rust was smaller and cleaner before async and const, then by all means, say that! But the post doesn't, leaving us to try to figure out what was meant.
This doesn't strike me as much simpler than Rust; it has most of the features listed above. I know a lot of people don't like RAII (which Rust has and Austral doesn't) because they want every function call to be visible at the call site, but replacing that with linear types, whatever their other virtues, does not make the language easier to learn; implicit destructor calls aren't hard to understand as a concept, once you've got your head around the notion of a value with a specifically bounded lifetime, whereas fighting the linearity checker seems likely to be an even greater speed bump for new users than fighting the borrow checker (which Austral still has).
> So what exactly is the "much smaller and cleaner language struggling to get out" of Rust? If I'm understanding the post right, that language still has references, lifetimes, traits, enums, etc., because all of those features cohere; you can't remove just one and expect the rest of the language to still work. Once you grant all those features, your language isn't much smaller or cleaner than Rust; your language pretty much is Rust.
I think there's an argument to be made that you could in fact make a simpler language than Rust while keeping the core concept. This variant of the language would remove:
- the Copy trait
- reborrowing
- deref coercion
- automagic insertion of `into_iter` in loops
- automatic call to drop at the end of scope (instead you'd have to call it by yourself or get an compiler error)
- trait bounds having an automatic `:Sized` bound by default.
- lifetime elision
- no “match ergonomics”
- and likely a few other pieces of “magic” that can be hard for beginner to grasp, but that's what I had at the top of my head.
This language would be much simpler, by having fewer concepts and a lot less “magic”, but it would also be much more cumbersome to use in a day to day basis, as all of the above are clever workaround designed to make common tasks as straightforward as possible, and I don't think anyone would prefer to use it than using Rust itself. It may be useful as an instructional tool when introducing Rust to students though.
I'm actually unconvinced you could do without `Copy`. It's both in the core of the language and essentially required for stuff like shared references to work correctly. Copying could be replaced by a keyword instead of happening implicitly, but that's different from removing the concept entirely from Rust.
The rest, sure, you could do without (reborrowing can't be emulated but I don't think it's strictly necessary to write real code). I'd add catchable exceptions and the use of traits instead of explicit modules as things that I think tremendously complicate the language semantics and are almost certainly not strictly necessary to achieve Rust's language goals.
You can already write a Rust program that never relies on Copy, by explicitly calling .clone() whenever you need to copy something. It's just that this would be insane.
Do those features actually cause difficulty for anyone other than compiler engineers, compared to not having them? I haven't personally seen, e.g., newbies stumbling over them; they're actually designed remarkably well to fade into the background and Just Work (i.e., you don't notice they're there, but you definitely would notice if they went away). Yes, there's something to be said for minimalism in language design, but Rust without those features still isn't very minimalistic, so dropping them would seem to bring about most of the costs of minimalism without the benefits.
> Do those concepts actually cause difficulty for anyone other than compiler engineers, compared to not having them? I haven't personally seen, e.g., newbies stumbling over them;
Pretty much all of the features in the list are things that either I have struggled with when learning Rust (the Copy trait and lifetime elision), or I've seen beginner struggle understanding (both new hires, or students at the place when I gave Rust lectures).
you were reading very closely, well done. yes, that is my claim, Rust was smaller and cleaner before async and const. I was so indirect about it because many of my best friends work on those features and I wasn’t sure how to word it. fortunately Matklad has worded it very well on the other site: 2015 rust was a more complete language that cohered better, but the vision of Rust is not to cohere perfectly, it’s to be an industrial language that is useful even if it’s not beautiful.
Yeah, in that case I think the link to boats's work obscures the point a bit.
I take what might be a slightly different read of matklad's point; I don't think Rust has much compromised its vision in terms of which broad features to support, but it has on a couple occasions chosen to ship something that wasn't perfect because being useful requires taking only a bounded amount of time to iterate on design.
So Rust 1.0 shipped without async, even though it was known to be needed for some of Rust's core use cases, because it was too far from being ready and it wouldn't do to wait forever. Once that decision was made, it had implications for how async could work; in particular, really doing it right requires linear types, but this wasn't appreciated when Rust 1.0 shipped and it's not a backwards-compatible change, so by 2018 it was off the table. The choice was, do async in a way that works with the existing design decisions, at the cost of some elegance, or don't do it at all. The former choice is not just more "industrial", I would argue that it coheres better, because waiting for multiple events at the same time is a core feature that a language for foundational software has to have, and the combinator-based approach that people were using in 2018 cohered poorly with the rest of the language (e.g., requiring unnecessary heap allocations). So this wasn't really a compromise to coherence.
(This also happened on a lesser scale when async/await first shipped—e.g., specific "async" syntax instead of a more general coroutine feature—because of eagerness to ship something that year. boats has claimed that this was a matter of existential survival for the language; I'm not sure I agree. But while async/await is a bit less conceptually pure than fully general coroutines, I don't believe that any of today's common complaints about async are downstream of the decision at that time to try to ship something quickly; there don't seem to have been a lot of obvious mistakes from then.)
(My understanding is that const has a similar story but I'm less familiar with the design space there, because people haven't exhaustively chronicled its history like they've done with async, perhaps because it's not as heatedly controversial.)
> in particular, really doing it right requires linear types, but this wasn't appreciated when Rust 1.0 shipped and it's not a backwards-compatible change, so by 2018 it was off the table.
It was pretty much off-the-table well before that, because a usable implementation of linear types requires being able to ensure the absence of panics. (A panic must unwind the stack, which amounts to automatically running drop implementations.) The two issues are quite closely linked, and hard to address in isolation.
I think an interesting component is that you might also want “semi linear types”: types which are purportedly linear but can be dropped as an unwinding backstop.
For instance if you’re dealing with database transactions you probably want to make it explicit whether you commit or rollback, but on panic you can likely allow the transaction to be cleaned up automatically.
This is one of many things that could have been done to solve the unwinding-through-linear-types problem, if it were still possible to make backwards-incompatible changes to the language.
I think we are using different meanings of the term "cohere" and I am not sure how to reconcile them. I agree that Rust with async is a more useful language. I don't think being useful implies anything about how coherent a language is (I would point to bash and perl as examples of useful languages with very little coherence). "Coherence" to me means that all the features fit together tightly and are designed with each other in mind, and I don't think that's the case for async and const in Rust—simply because they aren't finished being designed.
Your point on coherence is similar to the perspective of an ex-C++ maintainer. This video came out a decade ago https://www.youtube.com/watch?v=KAWA1DuvCnQ&t=2530s and I feel his lesson went unheeded. It's relevant to the bigger and more dangerous concept of Conceptual Integrity by Fred Brooks.
I know I'm biased, but Rust is the closest thing we have to a perfect programming language. Is the borrow checker a pain in the ass? Yeah. But is it necessary? Absolutely. Imagine writing the same buggy program in C, deploying it, and then it blows up at runtime—you still have to fix it, right? A bug is a bug, and it needs fixing. The difference is Rust forces you to deal with it before you even get a binary, while with C you might get a 3 a.m. wake-up call trying to figure out what went wrong. So it’s not that Rust is harder, it’s just different. It takes a paradigm shift in how we think about writing safe and secure code. Change is uncomfortable in general for humans and that paradigm shift is precisely why most (I hope not) people feel this way about Rust.
* I think Rust gives the compiler too much freedom to choose whether or not to apply Deref (and in what order). The whole .into() and From trait allows the compiler to perform arbitrary type conversions to make the code work (the standard library is full of similar "convenience" traits and functions). All of these tend to hide the types of a objects, making it hard to map a function call to an implementation (though a good IDE can help with that).
* I think implicit return values is a misfeature as it makes flow control implicit, hiding it from programmers reviewing the code. I'm also not fond of the question mark operator, though syntax highlighting helps a lot with that.
* Rust modules are generally too small so that you need hundreds of dependencies to do anything useful. Each of which you need to separately vendor and periodically update if you need deterministic builds.
* Async Rust is a hot mess right now
I didn't say perfect, I said closest to perfect. Regarding the implicit return types, it's all a matter of taste. I think they're very clean, but Rust is not forcing you to be implicit—you can be explicit if you like. e.g., if a function's return type is String, then whether you write the implicit "Hello".into() or the explicit "Hello".to_string() or String::from("Hello") is entirely up to you, and Rust will not complain.
> Is the borrow checker a pain in the ass? Yeah. But is it necessary?
You've missed the primary point of the post entirely. Borrow checker per se is not the problem; it's the sheer amount of everything. There's multiple ideas of perfection. Those of us to have very much enjoyed ostensibly imperfect Rust of 2018 find this particular, current flavour unappealing. It may as well be capable tool in deft hand, however, as-is the case with everything in life, you cannot help but ask yourself THE question; is it worth my effort? For me personally, if I were looking for better C/C++ in 2025, I would choose Zig before Rust any day of the week (one exception being Postgres stuff, pgrx ecosystem that is really special!)
But then again, anything beats writing C for a living.
Affine types, variance, higher-rank trait bounds, phantom data, MaybeUninit, and the whole macro and proc-macro systems are some examples of concepts that I found to be challenging when learning Rust.
Dyn-safety is another but I had encountered that previously in Swift.
I often encounter people that want to learn a programming language and ask if they should pick Rust as their first language. My answer is universally: NO.
Learning a first programming language is hard. And Rust will only make it harder since all you're going to do is debug compiler errors all day and never even see your program actually run until it's "perfect". This will be incredibly frustrating and you'll give up.
I always tell people to start with Python, JavaScript, or Lua. You can make something fun and interesting for yourself, like a game, and get immediate feedback and you can iterate quickly.
Not my experience. We have an ML engineer at work that has only worked with python. He wanted to contribute to our rust codebase so I spent about an hour going over the basics of rust with him, and answered questions as they came up, and he was able to get up and running very quickly. He quickly became quite productive in our rust codebase.
And theoretically, I'm not sure it's that very fun to run your game, have it crash because you passed a string to a function that expected an integer, stare at a backtrace, see where that variable came from, go up the call stack a few levels, and then realize "oh, i forgot to parse that string after the user typed it in". I'd rather get a compile error that underlines the problematic code and says "this is a string, but the function expected an integer". It may be debugging compiler errors all day, but the alternative is debugging runtime errors all week.
I'm not convinced. I want to see Rust as First Language attempted by somebody good. The staff who taught me the Standard ML of New Jersey are retired, and the current programme head is a friend who disagrees with me strongly about how this entire discipline should be approached, but in principle I feel it ought to be interesting & might be extremely productive for the cohort.
I think worst case it's a "sink or swim" course where a large fraction of the cohort wash out and that's bad for numbers (which will annoy bean counter but for a prestige university might actually be OK medium term) and best case the wash out rates are no different but the good students are receiving a lot more value than for today's First Language which is (at the university I'm thinking of) Python.
I think the move assignment semantic and the use of "const" to mean constant are examples of things where Rust is significantly penalized by having older languages teach their insane choices as just "How it is" so that then later a Rust course needs to have students un-learn this stuff when it would be easier to begin there.
It has been attempted! I've taught if to over 400 beginners now and cannot overstate how it's not just possible but actually works pretty damn well. And students instantly have a background from which they can learn other languages too.
In general I recommend people stay away from static typing for a first language. I'm a static typing booster, but for beginners it is unnecessarily confusing.
Compiler errors are by nature often counterfactual: "compiler couldn't prove that this wasn't none" or "once you pass through this generic method it's no longer known that your list of Animal only contains members of Dog" is usually more confusing and less tractable than a concrete test case that craps out your program. Then you can start poking away with prints at the stacktrace.
If you step through every single line of your execution carefully and inspect all the data, you're virtually certain to unstick yourself. I can't say the same if you get some gobbledygook from the compiler.
Rust wouldn't be a bad language if humans were able to take it in all at once. The problem is, nobody learns like that and Rust is very hard to iterate on if you don't know a lot of the key concepts.
The problem with this is you'll necessarily have to unlearn something or be frustrated when you achieve some level of proficiency in other languages since it still requires all those concepts which don't exist anywhere else.
There is an easy subset of Rust, namely the "pure functional" subset that doesn't use reference types at all and just passes everything by value. The use of & ("shared") references is only marginally harder. All of these features can be introduced step-by-step, together with basic notions that are not exclusive to any programming language. I'm aware that this is not how Rust is generally taught, but it's worthwhile to attempt such an approach.
I'm the author of the quote at the top of this blog post. I've at this point taught rust as a first language to over 400 people and am very amused by the claims in this thread and it's comments. After a few years of doing this I'm not just convinced it's possible, but have seen a lot of evidence of it working pretty well.
I would suggest Python or Lua before JS if you want to learn formally, such as following a book/series/class. JS (and TS) just have so much flexibility and many functionalities have been enhanced over the years that depend on the runtime context and build tooling in some cases.
Don't get me wrong, I love JS/TS since before the "Good Parts" book ever came out. The only advantage it has as a first language is you can start tinkering directly in the browser... Which is something I use to this day... the debug console in the browser, I can exercise a generated API client before the UI features are flushed out.
If you want to learn with the intent of "I want to build $THING." then JS/TS is probably a great language to start with... you will probably want to read something like a for dummies book to start, then bootstrap with a coding ai... and tinker until it works. Note: don't do this for anything security critical when starting out.
I really don’t think so, but it probably depends on which languages you’ve used in your life.
I’ve spent most of my professional time in C, Ruby, and a decent amount of time dabbling around in functional languages. All of the core bits of Rust from this post more or less clicked immediately for me. The other features are there, but they’re typically not something you need to internalize to comprehend Rust as a whole.
The biggest hurdle—as for most people—was learning what the borrow checker is really trying to teach you about the structure of your programs and internalizing those lessons. But having some experience with RAII and good practices around manual pointer ownership in C helped pave a pathway toward understanding those concepts.
Nah not really. If you are in C/C++ land you know exactly what problem Rust solves. If you only even known GC languages then yeah it will be a up battle hill.
The creator has said it's not. It's compiler is in rust though. But Gleam is an entirety different language in terms of paradigms and target runtimes. It can't really replace rust
* Same everything-is-an-expression style. Blocks evaluate to the last expression in them.
* /// Doc comments
* ML style pattern matching
* Result type
* 'todo', 'panic'
"Whhaaa but you can find those features in other languages! You literally said ML-style you big idiot!!"
Indeed but Rust massively popularised a lot of those features (how many people actually use ML?) and the number of them makes it almost impossible that it isn't at least strongly inspired by Rust.
Interestingly they have fixed the tuple syntax issue - it's #(a, b, c) - but have copied the match pattern matching mistake where you can write what looks like a variable name and it becomes a pattern. That mistake has been known since the 70s.
Wings3D is a 3D modeller written in Erlang so I would assume you could use Gleam for that as well given that it's the same runtime. Although I think it's probably not the best tool for the job.
This feels like a fairly uncharitable take. The author conveniently left out all of the things needed to understand that JavaScript version, including async/await, promises, modules, string interpolation, lambda syntax, not to mention the runtime that is running this, etc.
You also don’t have to start with a program that invokes 20 concepts at once, every one of those rust concepts can have its own hello world, introducing one concept at a time. Frankly several of them are just fundamental CS concepts.
> The author conveniently left out all of the things needed to understand that JavaScript version, including async/await, promises, modules, string interpolation, lambda syntax, not to mention the runtime that is running this, etc.
I think I disagree that you can have a Rust program that doesn't use all these concepts, at least outside of very basic tutorial material. You will very very quickly run into compiler errors that mention them the second you write your own program.
> The author conveniently left out all of the things needed to understand that JavaScript version, including async/await, promises, modules, string interpolation, [...] not to mention the runtime that is running this, etc.
I mean both languages (and programs) have modules, string interpolation, lambda syntax, and a compiler/interpreter. The only thing they really left out is Promises. And you're iterating over an array so there's no understanding of iterators needed. You can write Python for a long time without ever learning about __iter__. In the example Rust program the iterator is exposed. I think if the Rust version only used the for syntax you could say you don't need to know about iterators.
Javascript has exceptions (the article even mentions them, but seems to assume that they're somehow intuitive?) whereas Rust doesn't. And the Javascript "first-class function" syntax isn't really objectively simpler than the Rust lambda syntax, which the article seems to assume.
As I read it,
the author is making a point about explicit vs implicit knowledge.
In the Rust version the compiler will ~yell at you~ politely point out where your code won't work,
while the js version will ... just run, but maybe not work?
> Let’s look at a Rust program that does something non-trivial: ... (a bunch of highly specific explanations of deeply technical details from the very short program source code)
what does this program actually do?
all of this extraordinarily subtle analysis, about rust language specific properties, specific to this individual program, and no actual summary or description of what the program is supposed to do!
what it does is print out a line whenever a file, matching certain criteria, is modified. that's it.
and as such it's an almost pitch-perfect example of exactly what's difficult with rust as a language. what this program does is trivial to describe and should have a commensurately simple implementation. but rust makes the programmer care about a dozen? dozens? of details that have no relevance to the problem domain at hand.
for people who are just hungry for complexity this is an exciting challenge that they can tackle and solve! but that complexity, that challenge, is self-inflicted, almost always incidental and unnecessary work
> and as such it's an almost pitch-perfect example of exactly what's difficult with rust as a language. what this program does is trivial to describe and should have a commensurately simple implementation. but rust makes the programmer care about a dozen? dozens? of details that have no relevance to the problem domain at hand.
As the other comments have said, in other languages you face the same problems, those languages just won't help you handle them. Not all filenames are printable, but most languages don't help you deal with this. Almost all languages have some way that functions can fail and not return the kind of value you wanted - in Rust this is done by having a return type that shows it might return a different kind of value, in other languages this is done by having a novel concept of "exceptions" and you have to worry about whether every function is "exception safe", which is not actually simpler even if it looks like it at first glance. Etc.
But the implementation is simple, especially for such a high-performance language. It fits on less than a single printed page. What more could you expect?
simple isn't measured in terms of SLoC, it's measured in terms of cognitive complexity
the article itself says, clearly, that the (short) rust program requires knowledge of a significant number of non-trivial language features, as well as the behavioral outcomes of all of their combinatorial interactions -- approximately none of which are relevant to the problem that is being solved by that program!
what I expect is that a language adds as little cognitive complexity (overhead) as absolutely possible, when it is used to express a solution to a given problem
I find Rust pretty cohesive and consistent in its semantics. Accomplishing different tasks involves less sugar than other languages like this article seems to focus on.
Generally all of the interfaces conform to the patterns in the mem module. If you want to understand the structure of everything else unambiguously it would be best to start there: https://doc.rust-lang.org/std/mem/
Having written a moderate amount of both Rust and TypeScript they seem different but I wouldn't say projects in one are significantly simpler than projects in the other. Rust itself feels a little more complicated than TypeScript, but more carefully thought out. TypeScript has more cruft and legacy footguns. Rust tooling is much better (and faster). For cranking out web front end code TypeScript will get the job done faster, most other needs I run into are easier solved using either Rust or Python when starting fresh is an option.
> There’s a famous quote by Bjarne Strousup that goes “Within C++, there is a much smaller and cleaner language struggling to get out.” Within Rust, too, there is a much smaller and cleaner language struggling to get out: one with a clear vision, goals, focus. One that is coherent, because its features cohere. This post is about that language.
The problem is that using this "core" of a language never works. Either you need a feature or a library that touch the bad parts you are trying to ignore.
Still, it's much easier if 95% of the codebase uses just the core, and the parts that don't are relegated to known here-be-dragons areas, that are always reviewed by a couple of more experienced engineers.
I think Rust is the biggest winner that has from LLM support. When I got an compile error, it is painful without LLM to reverse-thinking how to re-write something and why it needs to be written that way.
EDIT: I’m leaving the comment up so the replies make sense, but I completely missed the point here. That’s what I get for writing dismissive hacker news comments on my lunch break!
I find it kind of hard to take this seriously since the JS snippet has a glaringly obvious syntax error and two glaringly obvious bugs which demonstrate that the author didn’t really think too hard about the point they’re trying to make.
I understand the point they’re trying to make, that being that rust forces you to explicitly deal with the complexity of the problem rather than implicitly. It’s just that they conveniently ignore that the JavaScript version requires the programmer to understand things like how async await works, iterators (which they use incorrectly), string interpolation, etc. Just using typescript type annotations alone already gives the js version nearly all the explicitness of rust.
> I understand the point they’re trying to make, that being that rust forces you to explicitly deal with the complexity of the problem rather than implicitly
I read it again and understand what you mean. I apologize for commenting like that so quickly, I was on my phone and typed that comment out before I really had time to digest the contents.
I've taught programming to some people who had no previous experience with it, and I can tell you that the list of concepts you have to learn at once is basically as long for Python, the quintessential "beginner" language.
The author's argument feels intellectually dishonest for that reason. Especially glaring is the comparison to JavaScript. The latter has an insane amount of concepts to deal with to do anything, including some truly bizarre ones, like prototypes.
Rust is hard to learn, IMO, for precisely two reasons:
1) The borrow checker is in an uncomfortable place, where it's dumb enough that it rejects perfectly valid code, but smart enough that it's hard to understand how it works.
2) As the author points out, there are a lot of levers available for low-level control, precise allocation, etc.
With respect to the second point, the author describes a language he'd like to see: green threads, compiler deciding on allocation, fewer choices and easy thread safety.
This language already exists (minus algebraic types). It's called Go. It's perfectly fine, well-designed and good for beginners. Some people don't like its aesthetics, but that's not reason enough to invent it again, only with Rust-inspired syntax.
Go is not thread safe. It even allows data races in its own runtime. Go 1.25 fixed a nil-checking error that has been sitting in the runtime for two years.
Generally, Go will let you compile just about anything when it comes to channels, mutexes (or not..), WaitGroups, atomics, and so on. It usually compiles as there are no static checks, and then just fails at runtime.
That’s a good point. I remember that in the earlier days of Go, there was a lot more optimism about the compiler’s future ability to statically decide more about the program than it currently does. It’s unfortunate that the language never got there.
Sadly, given Google’s new culture and all the people who have left, it seems unlikely it’ll ever get there.
> the list of concepts you have to learn at once is basically as long for Python, the quintessential "beginner" language
IMO, Python is great. But deploying Python is more work than learning Rust, oh and the tooling is something that requires continual education: I hear UV is what all the serious python teams are using now. Better learn it or be left behind!
Meanwhile, vanilla Rust has everything you need. I'm glad I understood sunk cost fallacy and got out of Python after many months of coming up to speed with it.
I just watched an experienced programmer on twitch struggle for an hour trying to install a Python-written program using UV! The churn is real, and distributing python programs is still a mess.
that's very unfair towards the both uv (uv is great) and the absolutely atrocious state of Python tooling until basically 2025 (slow and brittle, if it was even installed - hello, debian derivatives). uv is the cargo of Python we've been waiting for and it is expected you won't need to learn anything else for a long time.
Rust without safety-by-default, and hence without the borrow checker, is not Rust. A smaller Rust should either remove unsafe constructs (and then you get something more like a new Java/Scala/Kotlin except with aliasing XOR mutability) or keep the pointer/reference distinction (which Zig doesn't have).
Zig is more like a smaller C++ than a smaller Rust.
Another post about how Rust is not to their liking because it has things there familiar language does not have. For example the author says exceptions are somehow "simpler" than Result types and it's a downside Rust does not allow you to get a value without checked for error. And complains about ? operator. I think just because you are more familiar with exception style programming does not make it simpler.
I think you misunderstand the direction the author is coming from - Jynn is definitely not a newcomer to Rust who doesn't understand why things in Rust are the way they are
Yep - I came away from this as someone who hasn't touched Rust yet as "Yes, it's complicated, but that's for good reasons, because all of these complicated things work very well together to make writing software better ... when that doesn't happen you have things like Pythons pattern matching which is yuck".
I didn't read the article as being particularly critical of Rust at all, only demonstrating that it has a lot of concepts that you need to know in a coupled way in order to do most functional things in the language. It just means it's a slightly harder language to learn.
I've experienced this myself when starting out and just wanting to make $THING in Rust. A lot of looking things up... then trying it out... then seeing the compiler errors.. then looking up WTF they mean when you don't have solid grasp on X, Y and Z in Rust. Then going down the rabbit hole of mut and clone in order to do what you want... often just cloning because it's easier, even if less performant, only later to understand (a)rc etc.
In another language, you may well get something to run, even if not correct, which can be far more satisfying to correct an incorrect behavior/bug then to have the compiler yelling at you from the start (slowly)... it's a slower and more painful feedback loop to work with. It isn't bad so much as different. That doesn't mean it isn't real, or that I or TFA are bashing Rust.
https://nodejs.org/api/fs.html#filename-argument
reply