Level 2 · The Rust Way

Ownership 👑

This is it: the idea that made Rust famous, the reason it needs no garbage collector, and the reason it can't crash from memory bugs. Take this lesson slow. It's worth it.

The problem every language must solve

Programs constantly create data in memory. Eventually that memory must be given back, or the program bloats and dies. Before Rust, there were two options:

Rust found a third way: the compiler figures out, at compile time, the exact moment each value is no longer needed, and frees it there. No janitor. No human error. To pull that off, Rust needs one simple system of rules. That system is ownership.

The three rules 📜

The whole law of ownership
  1. Every value has exactly one owner (a variable).
  2. When the owner goes out of scope, the value is dropped (freed).
  3. Assigning or passing the value moves ownership to the new place.

Rule 2 in action: scope is destiny

A scope is a region between braces { }. When a variable's scope ends, its value is cleaned up, instantly and predictably:

fn main() {
    {
        let toy = String::from("squeaky crab");
        println!("playing with {toy}");
    } // ← toy's scope ends HERE: memory freed, automatically

    // println!("{toy}"); // ❌ toy doesn't exist anymore
}

That's the entire garbage collector replacement: a closing brace.

Rule 3 in action: the move 📦➡️

Here's the moment that surprises everyone. Watch closely:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;                 // ownership MOVES from s1 to s2

    println!("{s2}");            // ✅ fine, s2 owns it now
    println!("{s1}");            // ❌ compile error!
}
error[E0382]: borrow of moved value: `s1`
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`
3 |     let s2 = s1;
  |              -- value moved here
5 |     println!("{s1}");
  |               ^^^^ value borrowed here after move

Why?! Think of the String as a balloon 🎈 and variables as hands. Rule 1 says only one hand may hold the string at a time. let s2 = s1; passes the balloon to the other hand. If Rust let both hands believe they own it, both would try to pop it at scope's end: a double free, one of the classic C crashes. Rust's answer: the old hand simply lets go. s1 is done, and using it is a compile error, not a 3am production incident.

Want two balloons? clone

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();   // an actual second copy, in new memory

    println!("{s1} and {s2}");  // ✅ both valid: two values, two owners
}
💡 Beginner's rule of thumb If the borrow checker is fighting you and you just want to move on: .clone() is honest, safe, and fine. Optimizing away clones is a hobby for later. Correct first, clever second.

Small stuff just copies 🍬

Numbers, booleans, and chars are so small they live entirely on the stack (fast, fixed-size memory), so Rust copies them instead of moving:

fn main() {
    let a = 5;
    let b = a;              // a plain copy, no move drama
    println!("{a} and {b}");  // ✅ both fine
}

Why the difference? A String keeps its text in heap memory (the big, flexible warehouse), and only a claim ticket lives in the variable. Copying tickets without copying the goods is exactly the double-free trap, so heap-backed values move. Simple values with no warehouse claim (i32, bool, char, f64…) are "Copy types": they just duplicate.

Functions move things too

Passing a value into a function follows the same rule; the parameter becomes the new owner:

fn devour(snack: String) {
    println!("nom nom: {snack}");
} // snack's scope ends → the String is freed here

fn main() {
    let cake = String::from("carrot cake");
    devour(cake);              // ownership moves into the function...
    // println!("{cake}");     // ❌ ...so cake is gone out here
}

Giving a value away every time you call a function would be exhausting, though. What if you could lend the cake without giving it up? That's exactly what Lesson 7 is about, and it's where ownership starts feeling natural instead of strict.

⚠️ Common stumbles
  • Using a variable after assigning it elsewhere: "value borrowed here after move." Fix: .clone(), or read on to Lesson 7 and lend instead.
  • Expecting numbers to behave like Strings: they don't; they're Copy types.
  • Trying to memorize the memory theory before feeling it. Do the exercises; hands teach faster than diagrams.
Exercise 1

Predict the verdict

Which of these compiles? Decide first, then test both in the Playground.

// Program A
let word = String::from("crab");
let copy = word;
println!("{word}");

// Program B
let num = 42;
let copy = num;
println!("{num}");
Reveal answer

A fails: String moves, so word is invalid after the assignment. B compiles: i32 is a Copy type; copy is a duplicate, and num lives on.

Exercise 2

Two fixes, your choice

Make this compile two different ways: once with clone, once by reordering the code so the move happens last.

fn main() {
    let motto = String::from("stay rusty");
    let banner = motto;
    println!("original: {motto}");
    println!("banner: {banner}");
}
Reveal solution
// Fix 1: clone
let motto = String::from("stay rusty");
let banner = motto.clone();
println!("original: {motto}");
println!("banner: {banner}");

// Fix 2: use motto BEFORE moving it
let motto = String::from("stay rusty");
println!("original: {motto}");
let banner = motto;
println!("banner: {banner}");
Exercise 3

The vanishing cake

Type out the devour example from above and confirm the error. Then change devour to return the String (fn devour(snack: String) -> String) and hand ownership back with let cake = devour(cake);. Clunky? Extremely. Remember this feeling; Lesson 7 exists to fix it.

Reveal solution
fn devour(snack: String) -> String {
    println!("nom nom: {snack}");
    snack   // hand it back (expression, no semicolon!)
}

fn main() {
    let cake = String::from("carrot cake");
    let cake = devour(cake);   // ownership out, ownership back
    println!("{cake} survived!");
}