In the previous tutorial, we learned variables, types, and functions. Now we tackle the most important concept in Rust — ownership.

Ownership is what makes Rust unique. It is the reason Rust has no garbage collector, yet never leaks memory. Every Rust programmer must understand ownership. Once you get it, the rest of Rust clicks into place.

Why Ownership Exists

Most languages manage memory in one of two ways:

  1. Garbage collector (Java, Kotlin, Go, Python) — A background process finds and frees unused memory. Simple for the programmer, but uses extra CPU and can cause pauses.
  2. Manual management (C, C++) — The programmer allocates and frees memory. Fast, but easy to make mistakes — use-after-free, double-free, memory leaks.

Rust takes a third approach:

  1. Ownership — The compiler tracks who owns each piece of data. When the owner goes out of scope, the data is freed automatically. No garbage collector. No manual free. No bugs.

The cost? You must follow ownership rules. The compiler enforces them. At first this feels strict, but it catches real bugs at compile time.

The Three Ownership Rules

Memorize these. Everything else follows from them.

  1. Each value has exactly one owner — a variable that owns the data.
  2. When the owner goes out of scope, the value is dropped — memory is freed automatically.
  3. Assignment transfers ownership — the old variable can no longer use the data.

Let’s see each rule in action.

Stack vs Heap — Where Data Lives

Before we go deeper, you need to understand where data lives in memory.

┌─────────────────────────────────┐
│           STACK                 │
│  (fast, fixed-size, automatic)  │
│                                 │
│  ┌───────────┐                  │
│  │ age: 25   │  ← i32 (4 bytes)│
│  ├───────────┤                  │
│  │ active: T │  ← bool (1 byte)│
│  ├───────────┤                  │
│  │ ptr ──────┼──┐               │
│  │ len: 4    │  │               │
│  │ cap: 4    │  │               │
│  └───────────┘  │               │
└─────────────────┼───────────────┘
┌─────────────────┼───────────────┐
│           HEAP  ▼               │
│  (slower, dynamic, flexible)    │
│                                 │
│  ┌───┬───┬───┬───┐             │
│  │ R │ u │ s │ t │             │
│  └───┴───┴───┴───┘             │
│                                 │
└─────────────────────────────────┘

Stack: Fast. Fixed size. Values like integers, booleans, and floats live here. When a function ends, its stack data is gone instantly.

Heap: Slower. Dynamic size. Values like String and Vec store their data here. A pointer on the stack points to the actual data on the heap.

Why does this matter? Copying stack data is cheap — just copy a few bytes. Copying heap data is expensive — you must allocate new memory and copy all the bytes. This difference is why Rust has move and copy semantics.

Move Semantics — Ownership Transfers

When you assign a String to another variable, ownership moves. The original variable becomes invalid.

fn main() {
    let name = String::from("Alex");
    let other_name = name;  // Ownership moves to other_name

    // println!("{}", name);  // ERROR: value used after move
    println!("{}", other_name);  // OK — other_name owns the data
}

What happens in memory:

BEFORE move:
  Stack                    Heap
  name: ──────────────► [ A | l | e | x ]

AFTER move:
  Stack                    Heap
  name: (invalid)
  other_name: ─────────► [ A | l | e | x ]

The heap data does not move. Only the pointer on the stack changes hands. The old variable name is marked invalid. If you try to use it, the compiler stops you.

Why Move Instead of Copy?

Imagine if both name and other_name pointed to the same heap data. When name goes out of scope, it frees the memory. Then when other_name goes out of scope, it tries to free the same memory again — a double free bug. This is a real security vulnerability in C and C++.

Rust prevents this by making moves the default for heap data. One owner. One free. No bugs.

The Library Book Analogy

Think of ownership like a library book:

  • A book can only be checked out by one person at a time.
  • When you give the book to someone else, you no longer have it.
  • When the person who has it leaves the library, the book goes back on the shelf (memory is freed).
fn main() {
    let book = String::from("Rust Programming");

    // Alex gives the book to Sam
    let alex_book = book;
    // Alex can't read it anymore — Sam has it

    // Sam gives it to Jordan
    let sam_book = alex_book;
    // Sam can't read it anymore — Jordan has it

    println!("Jordan reads: {}", sam_book);  // Only Jordan can use it
}

Copy Trait — When Values Copy Instead of Move

Not all types move. Simple types that live entirely on the stack copy instead:

fn main() {
    let age = 25;
    let other_age = age;  // Copy, not move!

    println!("age: {}", age);             // OK — still valid
    println!("other_age: {}", other_age); // OK — independent copy
}

Why? Copying an integer is just copying 4 bytes on the stack. It is so cheap that moving would be pointless.

Types That Implement Copy

These types copy automatically:

TypeWhy it copies
i32, u64, all integersSmall, fixed-size, stack-only
f32, f64Small, fixed-size, stack-only
bool1 byte
char4 bytes
Tuples of Copy types(i32, bool) copies, (i32, String) does NOT

Types That Move

These types move on assignment:

TypeWhy it moves
StringOwns heap data
Vec<T>Owns heap data
Box<T>Owns heap data
Any struct with non-Copy fieldsContains owned data

The rule: If a type manages heap memory, it moves. If it lives entirely on the stack, it copies.

Clone — Explicit Deep Copy

What if you DO want to copy heap data? Use .clone():

fn main() {
    let name = String::from("Alex");
    let other_name = name.clone();  // Deep copy — new heap allocation

    println!("name: {}", name);             // OK — still valid
    println!("other_name: {}", other_name); // OK — independent copy
}

What happens in memory:

AFTER clone:
  Stack                    Heap
  name: ──────────────► [ A | l | e | x ]
  other_name: ─────────► [ A | l | e | x ]  (separate copy)

Now there are two independent String values. Each owns its own heap data. Each will be freed separately.

Use .clone() when you need two independent copies. But be aware — cloning is expensive for large data. Rust makes you write .clone() explicitly so you see the cost.

Ownership and Functions

Passing a value to a function works like assignment — it transfers ownership.

fn print_name(name: String) {
    println!("Name: {}", name);
    // name is dropped here — memory freed
}

fn main() {
    let user = String::from("Alex");
    print_name(user);  // Ownership moves to print_name

    // println!("{}", user);  // ERROR: user was moved
}

This is a common surprise for beginners. After calling print_name(user), the variable user is no longer valid.

main()                     print_name()
┌──────────┐              ┌──────────┐
│ user ────┼── moves to ──► name     │
│ (invalid)│              │          │
└──────────┘              └──────────┘
                          name dropped at }

Copy Types Pass Without Moving

Just like assignment, Copy types are copied when passed to functions:

fn double(x: i32) -> i32 {
    x * 2
}

fn main() {
    let number = 42;
    let result = double(number);

    println!("number: {}", number);  // OK — i32 copies
    println!("result: {}", result);
}

Return Values Transfer Ownership Back

A function can give ownership back through its return value:

fn create_greeting(name: &str) -> String {
    format!("Hello, {}!", name)
}

fn main() {
    let greeting = create_greeting("Alex");
    println!("{}", greeting);  // greeting owns the String
}

You can also take ownership and give it back:

fn add_exclamation(mut text: String) -> String {
    text.push('!');
    text  // Return ownership back to caller
}

fn main() {
    let message = String::from("Hello");
    let message = add_exclamation(message);  // Move in, move back

    println!("{}", message);  // Hello!
}

This works but is clunky. In the next tutorial, we will learn borrowing — a way to let functions use data without taking ownership.

Scope and Drop

When a variable goes out of scope, Rust calls drop automatically:

fn main() {
    {
        let name = String::from("Alex");
        println!("{}", name);
    }  // name goes out of scope — drop is called, memory freed

    // println!("{}", name);  // ERROR: name doesn't exist here
}

This is deterministic. You always know exactly when memory is freed — at the closing }. No garbage collector deciding when to clean up.

Drop Order

Variables are dropped in reverse order of creation:

fn main() {
    let first = String::from("created first");
    let second = String::from("created second");

    // At end of main:
    // second is dropped first
    // first is dropped second
}

Common Compiler Errors

Error: Use of Moved Value

let name = String::from("Alex");
let other = name;
println!("{}", name);  // ERROR!
error[E0382]: borrow of moved value: `name`
 --> src/main.rs:4:20
  |
2 |     let name = String::from("Alex");
  |         ---- move occurs because `name` has type `String`
3 |     let other = name;
  |                 ---- value moved here
4 |     println!("{}", name);
  |                    ^^^^ value borrowed here after move

Fix: Use .clone() if you need both, or use the new variable instead.

Error: Use After Move to Function

fn consume(s: String) { }

fn main() {
    let data = String::from("hello");
    consume(data);
    println!("{}", data);  // ERROR!
}
error[E0382]: borrow of moved value: `data`
 --> src/main.rs:6:20
  |
4 |     let data = String::from("hello");
  |         ---- move occurs because `data` has type `String`
5 |     consume(data);
  |             ---- value moved here
6 |     println!("{}", data);
  |                    ^^^^ value borrowed here after move

Fix: Have the function return the value, clone before passing, or use references (next tutorial).

Error: Partial Move

let pair = (String::from("hello"), String::from("world"));
let first = pair.0;  // Moves first element
println!("{:?}", pair);  // ERROR: pair partially moved

Fix: Clone the element, or use references.

Summary

ConceptWhat HappensExample
MoveOwnership transfers, old var invalidlet b = a; (String)
CopyValue duplicated, both vars validlet b = a; (i32)
CloneExplicit deep copy, both vars validlet b = a.clone();
DropMemory freed when owner leaves scope}
Function passSame as assignment (move or copy)func(value)
Function returnOwnership transferred to callerlet x = func();

Source Code

View source code on GitHub →

What’s Next?

Ownership by itself is limiting. You don’t always want to move data around. In the next tutorial, we learn borrowing and references — how to let functions read or modify data without taking ownership. This is the other half of Rust’s memory model.

Next: Rust Tutorial #5: Borrowing and References