DEV Community

Cover image for How to Learn Rust Ownership and Borrowing (Without Fighting the Borrow Checker Forever)
Spartan Rich
Spartan Rich

Posted on

How to Learn Rust Ownership and Borrowing (Without Fighting the Borrow Checker Forever)

My first week with Rust, I spent most of an evening on one error. I had a Vec<String>, I passed it to a function that printed it, and then I tried to print it again in main. The compiler told me the value had been moved. I added & in random places until it compiled, felt clever for ten minutes, and hit the same wall the next day with a struct.

That's the usual story. People don't quit Rust because of syntax. They quit because ownership feels like a set of arbitrary rules the compiler enforces with error messages, and they try to memorize the fixes instead of the model behind them.

Here's the order I'd learn it in if I started again.

Get the three rules into your head first

The Rust Book states them plainly, and everything else follows from them:

  1. Each value has one owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value is dropped.

That's it. Moves, borrows and lifetimes are all consequences. When an error confuses you, ask which of the three rules you just broke. Nine times out of ten the answer is obvious once you look.

Understand moves before you touch references

This is the code that got me:

fn print_all(items: Vec<String>) {
    for item in &items {
        println!("{item}");
    }
}

fn main() {
    let names = vec![String::from("Ada"), String::from("Linus")];
    print_all(names);
    println!("{:?}", names); // error: borrow of moved value
}
Enter fullscreen mode Exit fullscreen mode

Passing names to print_all hands ownership to the function. When print_all returns, its parameter goes out of scope and the vector is dropped. There's nothing left in main to print.

Notice that the same thing does not happen with an i32. Integers, booleans, char and a few others implement Copy, so passing them copies the bits and the original stays usable. String and Vec own heap memory, so they move. Once that difference clicks, a lot of "why does this work here but not there" goes away.

Then learn borrowing as two kinds of permission

The fix for the code above is to borrow instead of move:

fn print_all(items: &[String]) {
    for item in items {
        println!("{item}");
    }
}

fn main() {
    let names = vec![String::from("Ada"), String::from("Linus")];
    print_all(&names);
    println!("{:?}", names); // fine
}
Enter fullscreen mode Exit fullscreen mode

I find it easier to think of references as permissions rather than pointers. &T means "you can look." &mut T means "you can change it, and nobody else can even look while you hold this." You can have as many look-only borrows as you want, or exactly one mutable borrow, never both at the same moment.

The classic error that teaches this:

let mut scores = vec![10, 20, 30];
let first = &scores[0];
scores.push(40); // error: cannot borrow as mutable
println!("{first}");
Enter fullscreen mode Exit fullscreen mode

It feels pedantic until you realize why. push can reallocate the vector, which would leave first pointing at freed memory. In C that compiles and crashes later. Rust refuses up front.

Since the 2018 edition, borrows end at their last use, not at the end of the block (this is called non-lexical lifetimes). Move the println! above the push and it compiles. That one detail explains a lot of code that "shouldn't" work but does.

Leave lifetimes for later

Beginners often try to learn lifetime annotations in week one, and it's a mistake. Most of the time the compiler infers them. You only need to write 'a when a function returns a reference and the compiler can't tell which input it came from:

fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() > b.len() { a } else { b }
}
Enter fullscreen mode Exit fullscreen mode

Read that as "the result lives at least as long as both inputs." Learn it when you actually hit the error. Not before.

Read the error messages slowly

Rust's compiler errors are unusually good. They point at where the value was moved, where it was borrowed and where you tried to use it again, often with a suggested fix. I used to skim to the red line and start guessing. Reading the whole message, including the notes, fixed more bugs for me than any blog post.

rustc --explain E0382 (or whatever code you got) prints a longer explanation with examples. Worth doing for the first few errors you see repeatedly.

Practice on small programs that force the issue

Toy exercises help. Rustlings has a whole section on move semantics, and it's the fastest way to get reps. After that, build something small where data has to be shared:

  • A to-do CLI that loads tasks from a file into a Vec<Task> and lets you edit them.
  • A word counter that returns a HashMap<&str, usize> borrowing from the input text.
  • A tiny linked list. It's painful on purpose, and it teaches you why Box, Rc and RefCell exist.

When you reach for .clone() to make an error go away, write down why. Sometimes cloning is the right call. Often it's a sign you wanted a borrow.

Make the model stick

My real problem wasn't understanding ownership once. I'd get it after a weekend, take two weeks off for work, and come back to the same moved-value error as if I'd never seen it. The rules are short, but the instinct for applying them fades fast if you don't revisit it.

What helped was keeping a short list of the ideas that kept tripping me up (moves vs Copy, shared vs mutable borrows, borrows ending at last use, when lifetimes need writing) and going back over them after a day, then a few days, then a couple of weeks. That's spaced repetition, and for a mental model like this it beats rereading chapter four every time you get stuck.

Recl's spaced repetition schedule

I build Recl, and this is the kind of learning it's for. You save a Rust talk from YouTube, a chapter of the Book or a blog post about the borrow checker, it writes a summary of the key points, files it under a topic like "Rust", and brings it back on a spaced repetition schedule so you get quizzed before you forget. A plain notes file and calendar reminders do the same job if you'd rather not use an app. The point is to see it again before the compiler has to remind you.


I build Recl, an AI learning app that saves the videos, reels and articles you learn from, summarizes them, sorts them into topics, and uses spaced repetition so you actually remember them.

Top comments (0)