Home
Back to blog

Rust Isn't Hard, You're Just Thinking About It Wrong

·Kevin Karsopawiro
rust
performance

Here's a take that might ruffle some feathers: Rust isn't actually difficult. It just refuses to let you ignore things that other languages happily sweep under the rug.

The borrow checker isn't some sadistic gatekeeper. It's asking you questions about memory ownership that you should have been asking all along. The discomfort you feel when learning Rust? That's the sensation of becoming a better programmer.

Let me explain.

The Ownership Question You've Been Avoiding

Consider this code that trips up every Rust beginner:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
    println!("{}", s1); // Error: value borrowed here after move
}

The instinct is to fight this. "Just let me use the variable!" But pause for a moment and think about what's actually happening.

s1 points to heap memory containing "hello". When we write let s2 = s1, the language has to make a choice:

Option A: Deep copy. Clone the entire string. Now you have two independent copies on the heap. Works fine, but expensive. What if that string is 100MB?

Option B: Shared reference. Both variables point to the same memory. But now who's responsible for freeing it? If s1 goes out of scope first and deallocates, s2 is pointing at garbage. If both try to free it, you've corrupted the heap. Welcome to use-after-free bugs.

Option C: Move semantics. Transfer ownership. s2 takes over, s1 becomes invalid. One owner, clear responsibility, no ambiguity.

Every language picks one of these. Python and JavaScript pick Option B and add garbage collection to manage the mess. C++ lets you pick any of them and hopes you choose wisely (narrator: they often don't). Rust picks Option C and makes the choice explicit.

The borrow checker isn't being difficult. It's being precise.

Drake meme:

What Other Languages Are Hiding From You

Let's look at some innocent Java code:

public List<String> processItems(List<String> items) {
    List<String> filtered = items.stream()
        .filter(item -> item.length() > 5)
        .collect(Collectors.toList());

    List<String> result = filtered.stream()
        .map(String::toUpperCase)
        .collect(Collectors.toList());

    return result;
}

Clean, readable, idiomatic Java. Also: two heap allocations, eventual garbage collection pressure, and potential latency spikes when the GC decides to clean up.

You don't see any of this in the code. The language hides it. That's a feature for rapid development, but it's also why Java applications have those mysterious pause times in production.

Here's the Rust equivalent:

fn process_items(items: Vec<String>) -> Vec<String> {
    items.into_iter()
        .filter(|item| item.len() > 5)
        .map(|item| item.to_uppercase())
        .collect()
}

One allocation. Zero garbage collection. And you know this from reading the code because into_iter() consumes the original vector and the iterator chain produces a single result.

This isn't Rust being better. It's Rust being explicit. Whether that explicitness is worth it depends entirely on your use case.

The Compile-Time Guarantee

Here's where Rust genuinely shines: the compiler proves things about your program.

In C++, you can write code with use-after-free bugs, data races, and null pointer dereferences. The compiler will happily produce an executable. You'll find out about the bugs at 3 AM when production crashes.

In Rust, entire categories of bugs are impossible. Not unlikely. Not rare. Impossible. The type system and borrow checker mathematically prove their absence.

  • Use after free? Can't compile.
  • Data races? Can't compile.
  • Null pointer dereference? No null pointers.
  • Iterator invalidation? Can't compile.

When you refactor a large Rust codebase and it compiles, you can be genuinely confident it works. Not "probably works" or "works on my machine." The compiler checked the invariants.

This changes how you write code. You refactor fearlessly. You make large changes with confidence. The compiler has your back.

The Productivity Paradox

"But Rust is slow to write!"

Yes and no. You spend more time upfront making the compiler happy. Code that takes an hour in Python might take two hours in Rust.

But here's the thing: the Python code takes an hour to write and then needs debugging, edge case handling, and production monitoring for issues that manifest under load. The Rust code takes longer to write but tends to just work.

Language Time to write Time to debug Time to stabilize Total
Python 1 hour 3 hours 4 hours 8 hours
Rust 2 hours 30 min 30 min 3 hours

These numbers are made up, but the pattern is real. Rust front-loads the effort. Whether that tradeoff makes sense depends on how long your code needs to run and how much you value reliability.

For a quick script? Python wins. For infrastructure that runs for years? Rust's upfront investment pays dividends.

When Rust Is The Right Choice

Let's be honest about where Rust makes sense:

Systems programming. Operating systems, databases, browsers, game engines. Places where you need control over every byte and can't afford GC pauses.

Long-running services. If your code needs to run for months without memory leaks or mysterious crashes, Rust's guarantees are valuable.

Performance-critical paths. When milliseconds matter and you can't afford the overhead of a runtime.

Security-sensitive code. Parsers, network protocols, anything handling untrusted input. Memory safety isn't optional here.

WebAssembly. Rust has excellent Wasm support and produces small, fast binaries.

When Rust Is Probably Overkill

And let's be equally honest about where it's not worth the complexity:

CRUD web apps. Rails, Django, or even Node.js will get you there faster. The performance difference won't matter for most web applications.

Quick scripts. Python or bash. Don't overthink it.

Prototypes. When you're exploring problem spaces and expect to throw code away.

Teams without Rust experience. The learning curve is real. If you need to ship quickly with your current team, using familiar tools often beats using "better" tools.

The best language is the one that ships your project successfully. Don't cargo-cult Rust (pun intended) just because it's trendy.

The Learning Curve Is Real (But Finite)

I won't pretend Rust is easy to learn. The ownership model requires rewiring how you think about memory. Lifetimes can be confusing. The compiler errors, while helpful, can be overwhelming at first.

But here's the thing: it's a one-time investment. Once ownership clicks, it clicks forever. And the mental model you develop transfers to other languages. You'll start thinking about memory ownership in Python and JavaScript too.

Some practical advice:

Read "The Rust Programming Language" book. It's free, it's excellent, and it's the fastest path to understanding. Don't skip chapters.

Start with small projects. CLI tools, Advent of Code, small utilities. Build muscle memory before tackling complex systems.

Trust the compiler. When it rejects your code, it's usually right. Read the error message carefully. Rust has some of the best error messages in any compiler.

Don't fight the borrow checker. If you're constantly battling it, you're probably trying to write Java/Python patterns in Rust. Let go and learn the idiomatic approaches.

The Transferable Skill

Here's something unexpected: learning Rust makes you better at every other language.

After understanding ownership, you'll think about it in Python. "This function receives a list. Can it modify it? Should it? Who owns this data?" These were always valid questions. Rust just forces you to answer them.

After understanding zero-cost abstractions, you'll notice allocation patterns in Java. You'll spot GC pressure before it becomes a problem.

After understanding data race prevention, you'll be more careful with concurrent code in Go or JavaScript.

Rust doesn't just teach you Rust. It teaches you to think more precisely about programs in general.

The Bottom Line

Rust isn't hard. It's honest. It surfaces complexity that other languages hide.

That honesty has a cost: slower initial development, steeper learning curve, more explicit code. But it also has benefits: memory safety, predictable performance, fearless refactoring.

Whether the tradeoff is worth it depends on what you're building. For the right problems, Rust is genuinely excellent. For the wrong problems, it's unnecessary friction.

Choose wisely. And if you do choose Rust, embrace the borrow checker. It's not your enemy. It's a very picky friend who saves you from yourself.