← Back to articles

Rust features I like

I started learning Rust in 2019. I have to admit the learning curve was steep, and even now, writing Rust by hand without an LLM takes me time.

But in this new AI era, Rust is becoming the language of choice for backend software projects. Here are the features I like, and how the compiler helps when an LLM writes the code.

Ownership and borrowing

Every value has exactly one owner, and is freed when that owner goes out of scope, at a point the compiler knows statically: no garbage collector, nothing to free twice or forget to free. To use a value without taking it, borrow it with &.

The borrow checker allows many readers or one writer, never both. That rules out dangling pointers and double frees, and since aliasing plus mutation is also what a data race needs, most of thread safety comes along with it. Send and Sync cover the rest. Leaks and deadlocks remain possible, and unsafe opts out.

Immutability by default

Bindings are immutable unless you write mut. The keyword is small, but it puts mutation in plain sight: at the declaration, and at the call site passing &mut. RefCell and Mutex are the escape hatch, moving the check to runtime.

No null, few exceptions

Absence is a type: Option<T>. A String is always a real string, an Option<String> is the one you check first.

Failure is a value too: Result<T, E>, propagated with a single ?. Matching must cover every variant, so adding one becomes a list the compiler hands you rather than a search through the project. panic! stays for the unrecoverable case, and unwrap is the shortcut back to it.

Zero-cost abstractions

Generics are written once against the traits they require: fn largest<T: Ord>(items: &[T]) -> &T takes a slice of integers or a slice of strings with a single body.

Iterators are what I reach for every day. items.iter().filter(|i| i.active).map(|i| i.id).collect::<Vec<_>>() says what it does in one line, and nothing runs until collect asks.

Both compile down to what you would have written by hand: generics are monomorphized, the chain becomes one loop. The cost is real, but it is paid at build time, in compile time and binary size. The readable version is usually the fast one, so there is no pressure to write the ugly one.

Compiler errors as a feedback loop

Errors name the rule that was broken, show the lines that conflict, and often draft the fix as a diff. That is useful to me and more useful to an LLM, which has no colleague to ask. It writes this, because the equivalent in Python or JavaScript is perfectly fine:

fn greet(name: String) {
    println!("Hello {}", name);
}

fn main() {
    let name = String::from("Thomas");
    greet(name);
    greet(name);
}

rustc does not just say no:

error[E0382]: use of moved value: `name`
 --> greet.rs:8:11
  |
6 |     let name = String::from("Thomas");
  |         ---- move occurs because `name` has type `String`, which does not implement the `Copy` trait
7 |     greet(name);
  |           ---- value moved here
8 |     greet(name);
  |           ^^^^ value used here after move
  |
note: consider changing this parameter type in function `greet` to borrow instead if owning the value isn't necessary
 --> greet.rs:1:16
  |
1 | fn greet(name: String) {
  |    -----       ^^^^^^ this parameter takes ownership of the value
  |    |
  |    in this function
help: consider cloning the value if the performance cost is acceptable
  |
7 |     greet(name.clone());
  |               ++++++++

One error carries the rule that was broken (String is not Copy, so passing it gives it away), the two lines that conflict, and two ways out with the tradeoff named: take the parameter by reference, or clone, pre-drafted as a diff. The first fixes the design rather than the symptom: fn greet(name: &str), called with &name, is the signature that should have been written in the first place.

The first version is not a bug. It is a bad signature: a function that only reads its argument, asking to own it, so every caller must clone or give it up. Rust refuses to compile the second call, points back at the signature, and drafts the fix.