Why I’m Falling Out of Love with Rust
Rust has a reputation for being a modern, safe, and efficient systems programming language, and I initially bought into that hype. However, as I spend more time with Rust, I find myself increasingly frustrated with its complexity and design decisions. What once seemed like a breath of fresh air now feels like an exhausting exercise in fighting the language. Here are some of the biggest pain points that have made me reconsider my enthusiasm for Rust.
Syntax That Borders on Unreadable
Rust’s syntax is often heralded as expressive, but I find it to be unnecessarily complex. The sheer amount of symbols, brackets, and lifetimes annotations makes even moderately complex Rust code difficult to read at a glance. Compare this to Go, which is designed for simplicity, or TypeScript, which balances expressiveness with ease of readability.
Example: Rust vs. Go vs. TypeScript
Rust:
fn get_value(map: &HashMap<String, i32>, key: &str) -> Option<&i32> {
map.get(key)
}
Go:
func getValue(map map[string]int, key string) (int, bool) {
value, ok := map[key]
return value, ok
}
TypeScript:
function getValue(map: Record<string, number>, key: string): number | undefined {
return map[key];
}
Go and TypeScript prioritize clarity, while Rust’s syntax, with its explicit borrowing and references, feels significantly more convoluted.
Too Many Ways to Do the Same Thing
Rust prides itself on giving developers control, but sometimes it feels like too much control. There are often multiple ways to achieve the same result, each with different performance and safety trade-offs. Do I use Rc<T>
or Arc<T>
? Should I reach for Box<T>
or just take a reference? Do I need Cow<T>
? The choices are overwhelming, and instead of making development easier, they often introduce unnecessary cognitive overhead.
Example: Ownership Management in Rust vs. Zig vs. Go
Rust:
let x = Rc::new(5);
let y = x.clone();
println!("{}", y);
Zig:
var x = 5;
const y = x; // Simple assignment
print("{}", .{y});
Go:
x := 5
y := x // Simple copy
fmt.Println(y)
Zig and Go take a more pragmatic approach, avoiding unnecessary complexity while still ensuring safety.
Lifetimes: A Constant Struggle
Lifetimes are one of Rust’s most defining features, designed to eliminate entire classes of memory safety issues. However, actually working with lifetimes is often a frustrating, mind-bending experience. Compare this to Zig, which has manual memory management but avoids the complexity of lifetimes.
Example: Rust’s Borrow Checker vs. Zig’s Simplicity
Rust:
fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() > s2.len() {
s1
} else {
s2
}
}
Zig:
fn longest(s1: []const u8, s2: []const u8) []const u8 {
return if (s1.len > s2.len) s1 else s2;
}
Rust’s explicit lifetime annotations add complexity that Zig sidesteps entirely, making similar tasks much easier in Zig.
Async and Lifetimes: A Nightmare Combination
Rust’s async model, while powerful, adds an additional layer of complexity when combined with lifetimes. Borrowing across async
functions can quickly lead to tangled lifetimes and compiler errors that feel nearly impossible to resolve. Compare this to TypeScript, which makes async programming straightforward.
Example: Rust Async vs. TypeScript Async
Rust:
async fn fetch_data() -> Result<String, Box<dyn Error>> {
let response = reqwest::get("https://example.com").await?;
let text = response.text().await?;
Ok(text)
}
TypeScript:
async function fetchData(): Promise<string> {
const response = await fetch("https://example.com");
return response.text();
}
In TypeScript, async/await works as expected, while Rust introduces additional complexity due to its ownership model.
Manual Memory Management: Why Am I Doing This in 2025?
One of Rust’s biggest selling points is its lack of a garbage collector, allowing for precise memory management. But in day-to-day development, I often find myself wondering: why am I manually managing memory when other modern languages have figured this out?
Compare Rust’s manual memory management with Go’s garbage collection and Zig’s manual but ergonomic memory handling.
Rust:
let x = Box::new(5);
println!("{}", x);
Go:
x := 5
fmt.Println(x)
Zig:
const x = allocator.create(i32);
print("{}", .{x});
allocator.destroy(x);
Rust requires explicit heap allocation and deallocation, whereas Go handles it automatically, and Zig makes it explicit but ergonomic.
Conclusion
Rust is an impressive language with admirable goals, but it’s not without its flaws. Compared to Go, TypeScript, and Zig, Rust often feels like an unnecessary struggle. While some developers thrive in its strict, safety-first environment, I find myself more and more frustrated with its complexity and steep learning curve. As I continue to work with Rust, I question whether the benefits outweigh the headaches, and whether it’s truly the right tool for my needs.