Was Rust Worth It?
From JavaScript to Rust, three years in.
A few years ago, I dropped everything to focus 100% on WebAssembly. At the time, Rust had the best support for compiling into WebAssembly, and the most full-featured WebAssembly runtimes were Rust-based. Rust was the best option on the menu. I jumped in, eager to see what all the hype was about.
Since then, I (along with some other awesome people) built Wick, an application framework and runtime that uses WebAssembly as its core module system.
After three years, multiple production deployments, an ebook, and ~100 packages deployed on crates.io, I feel it’s time to share some thoughts on Rust.
The Good
You can maintain more with less
I am a massive proponent of test-driven development. I got used to testing in languages like Java and JavaScript. I started writing tests in Rust as I would in any other language but found that I was writing tests that couldn’t fail. Once you get to the point where your tests can run – that is, where your Rust code compiles – Rust has accounted for so many errors that many common test cases become irrelevant. If you avoid unsafe {}
blocks and panic-prone methods like .unwrap()
, you start with a foundation that sidesteps many problems by default.
The aggressiveness of Rust’s borrow checker, the richness of Rust’s type system, the functional patterns and libraries, and the lack of “null” values all lead to maintaining more with less effort spent in places like testing. I’ve maintained the 70,000+ lines of code in the Wick project with far fewer tests than I would need in other languages.
When you need to write tests, adding them on the fly is easy without thinking about it. Rust’s integrated test harness lets you add tests right next to code with barely a second thought.
I code better in other languages now
Programming in Rust is like being in an emotionally abusive relationship. Rust screams at you all day, every day, often about things that you would have considered perfectly normal in another life. Eventually, you get used to the tantrums. They become routine. You learn to walk the tightrope to avoid triggering the compiler’s temper. And just like in real life, those behavior changes stick with you forever.
Emotional abuse is not generally considered a healthy way to encourage change, but it does effect change nonetheless.
I can’t write code in other languages without feeling uncomfortable when lines are out of order or when return values are unchecked. I also now get irrationally upset when I experience a runtime error.
Clippy is great!
Clippy is Rust’s linter, but calling it a linter is a disservice. In a language where the compiler can make you cry, Clippy is more of a gentle friend than a linter.
The Rust standard library is enormous. It’s hard to find functions you know probably exist when so much functionality is spread across myriad granular types, traits, macros, and functions. Many Clippy rules (e.g., manual_is_ascii_check
) look for common patterns that stdlib methods or types would better replace.
Clippy has hundreds of rules that tackle performance, readability, and unnecessary indirection. It will frequently give you the replacement code when possible.
It also looks like (soon) you’ll finally be able to configure global lints for a project. Until now, you had to hack your solution to keep lints consistent for projects. In Wick, we use a script to automatically update inline lint configurations for a few dozen crates. It took years for the Rust community to land on a solution for this, which brings us to…
The Bad
There are gaps that you’ll have to live with
I questioned my sanity every time I circled back around to the Clippy issue above. Surely, I was wrong. There must be a configuration I missed. I couldn’t believe it. I still can’t. There must be a way to configure lints globally. I quadruple-checked when I wrote this to make sure I wasn’t delusional. Those issues are closed now, but they had been open for years.
Clippy’s awesome, but this use case is one example of many around the Rust world. I frequently come across libraries or tools where my use cases aren’t covered. That’s not uncommon in newer languages or projects. Software takes time (usage) to mature. But Rust isn’t that new. There’s something about Rust that feels different.
In open source, edge cases are frequently addressed by early adopters and new users. They’re the ones with the edge cases. Their PRs refine projects so they’re better for the next user. Rust has been awarded the “most loved language” for the better part of a decade. It’s got no problem attracting new users, but it’s not resulting in dramatically improved libraries or tools. It’s resulting in one-off forks that handle specific use cases. I’m guilty of that, too, but not for lack of trying to land PRs.
I don’t know why. Maybe the pressure to maintain stable APIs, along with Rust’s granular type system, makes it difficult for library owners to iterate. It’s hard to accept a minor change if it would result in a major version bump.
Or maybe it’s because writing Rust code that does everything for everyone is exceedingly difficult, and people don’t want to deal with it.
Cargo, crates.io, and how to structure projects
I modeled the Wick repository structure around some other popular projects I saw. It looked reasonable and worked fine until it didn’t.
You can build, test, and use what feels like a module-sized crate easily with Cargo. Deploying it to crates.io, though? That’s a whole different story.
You can’t publish packages to crates.io unless every referenced crate is also published individually. That makes some sense. You don’t want to depend on a crate that depends on packages that only exist on the author’s local filesystem.
However, many developers break large projects down into smaller modules naturally, and you can’t publish a parent crate that has sub-crates that only exist within itself. You can’t even publish a crate that has local dev dependencies. You must choose between publishing random utility crates or restructuring your project to avoid this problem. This limitation feels arbitrary and unnecessary. You can clearly build projects structured like this, you just can’t publish them.
Edit: Ed Page reached out to note that you can publish with local dev dependencies, as long as you don’t include a
version
inCargo.toml
Cargo does have excellent workspace support, though! Cargo’s workspaces offer a better experience managing large projects than most languages. But they don’t solve the deployment problem. Turns out, you can set workspaces up in any of a dozen ways, none of which make it easy to deploy.
You can see the problem manifest in the sheer number of utility crates designed to simplify publishing workspaces. Each works with a subset of configurations, and the “one true way” of setting workspaces up still eludes me. When I publish Wick, it’s frequently an hour+ of effort combining manual, repetitive tasks with tools that only partially work.
Async
Rust added async-iness to the language after its inception. It feels like an afterthought, acts like an afterthought, and frequently gets in your way with errors that are hard to understand and resolve. When you search for solutions, you have to filter based on the various runtimes and their async flavors. Want to use an async library? There’s a chance you can’t use it outside of a specific async runtime.
After two decades of JavaScript and decent experience with Go, this is the most significant source of frustration and friction with Rust. It’s not an insurmountable problem, but you must always be ready to deal with the async monster when it rears its head. In other languages, async is almost invisible.
The Ugly
Refactoring can be a slog
Rust’s rich type system is a blessing and a curse. Thinking in Rust types is a dream. Managing Rust’s types can be a nightmare. Your data and function signatures can have generic types, generic lifetimes, and trait constraints. Those constraints can have their own generic types and lifetimes. Sometimes, you’ll have more type constraints than actual code.
You also need to define all your generics on every impl
. It’s tedious when writing it the first time. When refactoring though, it can turn a minor change into a cascading mess.
It’s hard to make rapid progress when you need to tweak 14 different definitions before you can take a single step forward.
Edit to address external comments: The problem isn’t the expressibility, the problem is no language or tooling solution to reduce the duplication. There are frequent reasons to have the same constraints or refer to the same generic lists, but there’s no way to alias or otherwise refer to a central definition. I’m not sure there should be, but it doesn’t change the burden of duplication.
The Verdict
I love Rust. I love what it can do and how versatile it is. I can write system-level code in the same language as CLI apps, web servers, and web clients. With WebAssembly, I can use the same exact binary to run an LLM in the browser as on the command line. That still blows my mind.
I love how rock-solid Rust programs can be. It’s hard to return to other languages after you learn to appreciate what Rust protects you from. I went back to Go for a brief period. I quickly became intoxicated with the speed of development again. Then I hit the runtime panics, and the glass shattered.
But Rust has its warts. It’s hard to hire for, slow to learn, and too rigid to iterate quickly. It’s hard to troubleshoot memory and performance issues, especially with async code. Not all libraries are as good about safe code as others, and dev tooling leaves much to be desired. You start behind and have a lot working against you. If you can get past the hurdles, you’ll leave everyone in the dust. That’s a big if.
Was Rust worth it for us? It’s too early to tell. We’ve done amazing things with a small team but also had immense roadblocks. We also had technical reasons that made Rust more viable.
Will it be worth it for you? If you need to iterate rapidly, probably not. If you have a known scope, or can absorb more upfront cost? Definitely consider it. You’ll end up with bulletproof software. With the WebAssembly angle becoming stronger every month, the prospect of writing perfect software once and reusing it everywhere is becoming a reality sooner rather than later.