AnyLearn
All lessons
Programmingintermediate

Ownership: One Rule That Replaces the Garbage Collector

Every language must decide who frees memory and when. Rust answers with a rule enforced at compile time: each value has exactly one owner, and it is freed when that owner goes out of scope. This lesson builds the rule, shows what moving a value means, and explains what the approach buys and charges.

Updated · AI-authored, review-gated · how lessons are made

Not signed in: your progress and quiz score won't be saved.
Progress1 / 8

The question every language answers

Heap memory has to be released, and every language picks one of three answers.

Manual. The programmer calls free. Fast and predictable, and the source of the defect classes that dominate security reporting: use-after-free, double free, leaks. About 70% of the CVEs Microsoft assigned between 2006 and 2018 were memory safety issues, according to work by Matt Miller of the Microsoft Security Response Center presented in 2019, and Google has reported a comparable proportion for Chromium.

Garbage collection. A runtime determines when nothing references a value and reclaims it. Safe and convenient, at the cost of a runtime, unpredictable pauses, and memory overhead.

Ownership. Determine at compile time where each value's life ends, and insert the free there. No runtime, no pauses, and no manual calls.

The third is Rust's answer, and the entire language design follows from making it work. The cost is not paid at runtime; it is paid by the programmer, in the form of a compiler that rejects programs the other two would accept.

Full lesson text

All 8 steps on one page, for reading, reference, and search.

Show

1. The question every language answers

Heap memory has to be released, and every language picks one of three answers.

Manual. The programmer calls free. Fast and predictable, and the source of the defect classes that dominate security reporting: use-after-free, double free, leaks. About 70% of the CVEs Microsoft assigned between 2006 and 2018 were memory safety issues, according to work by Matt Miller of the Microsoft Security Response Center presented in 2019, and Google has reported a comparable proportion for Chromium.

Garbage collection. A runtime determines when nothing references a value and reclaims it. Safe and convenient, at the cost of a runtime, unpredictable pauses, and memory overhead.

Ownership. Determine at compile time where each value's life ends, and insert the free there. No runtime, no pauses, and no manual calls.

The third is Rust's answer, and the entire language design follows from making it work. The cost is not paid at runtime; it is paid by the programmer, in the form of a compiler that rejects programs the other two would accept.

2. Three rules, and the third does the work

The model is stated in three sentences.

Every value has a variable that is its owner. There can be exactly one owner at a time. When the owner goes out of scope, the value is dropped.

The first and third are unremarkable; C++ destructors do the same. The second is the one that changes the language.

A single owner means the compiler always knows exactly one place where a value dies: the end of its owner's scope. There is no need to determine whether anything else still refers to it, because nothing else can own it.

That is what makes the analysis decidable without a runtime. Reference counting asks a question at runtime because the answer depends on execution; ownership makes the answer a property of the source text.

{
    let s = String::from("hello");   // s owns the heap allocation
    println!("{s}");
}                                    // scope ends: drop(s) runs here

The closing brace is a free. The compiler inserted it, and it is in exactly one place.

3. Assignment moves rather than copies

The single-owner rule forces a consequence that surprises everyone arriving from another language.

let a = String::from("hello");
let b = a;              // ownership MOVES from a to b
println!("{a}");        // compile error: borrow of moved value `a`

Assignment did not copy the string and did not create a second reference to it. It transferred ownership, and a is no longer usable.

The reason is forced. If both a and b owned the same allocation, both scopes would end and both would free it: a double free. The alternatives are to copy the heap data, which is expensive and silent, or to invalidate the source. Rust invalidates.

The same applies to function calls. Passing a value by value moves it, and the caller can no longer use it unless the function gives it back.

Small values stored entirely on the stack are exempt. Integers, booleans, characters and similar implement Copy, so assignment duplicates the bits instead, which is cheap and creates no ownership question.

The distinction is exactly whether the value owns a resource. Bits on the stack are copied; anything owning a heap allocation is moved.

4. What a move actually does

A move is cheap. The three machine words describing the string are copied; the heap buffer is untouched. It costs the same as copying a pointer, which is what people usually expect assignment to do anyway.

The compile-time marking is the interesting half. Nothing is written to memory to record that a is dead. The compiler tracks it statically and refuses to generate code that reads a afterwards.

So the runtime behaviour is identical to a C pointer assignment, and the safety comes entirely from what the compiler refuses to emit. That is the general shape of Rust's guarantees: the checks happen at compile time and the generated code has no checking in it.

This is what the phrase zero-cost abstraction means here. The abstraction is ownership, and it costs nothing at runtime because it does not exist at runtime.

flowchart TD
A["let a = String::from(hello)"] --> B["a holds pointer, length, capacity"]
B --> C["Heap buffer: h e l l o"]
D["let b = a"] --> E["Pointer, length, capacity copied to b"]
E --> C
E --> F["a marked invalid at compile time"]
F --> G["Only b is dropped at end of scope"]

5. Giving it back, and cloning

Once moves are understood, the ergonomics question follows: how do you use a value after passing it somewhere?

Three answers, in ascending order of cost.

Return it. The function takes ownership and hands it back. Explicit, free, and clumsy enough that it is rarely the right answer.

fn shout(s: String) -> String { s.to_uppercase() }
let s = shout(s);            // moved in, moved back out

Borrow it. Pass a reference instead, which the next lesson covers in full. This is the normal answer and it is why references exist.

Clone it. Duplicate the heap data explicitly.

let b = a.clone();           // a stays valid; the buffer is duplicated

The design decision worth noticing is that clone is explicit and named. Languages with implicit copy constructors hide allocations inside ordinary-looking assignments, so a performance problem is invisible in the source. In Rust, every heap duplication appears as the word clone, and a profiler pointing at allocation traffic sends you to a greppable token.

Beginners over-use it to silence the compiler, which works and is worth un-learning: a clone added to satisfy the borrow checker usually indicates a borrow that should have been restructured.

6. The same rule frees everything else

Ownership is usually explained with memory, and its reach is wider. The rule is about resources, and memory is one.

A file handle, a socket, a mutex guard and a database connection all need releasing at a defined point, and all of them are owned by a value whose scope determines when.

{
    let f = File::open("data.txt")?;   // f owns the handle
    // ... use f ...
}                                      // closed here, guaranteed

No finally, no defer, no context manager, and no way to forget. The close happens because the value was dropped, and the value was dropped because the scope ended.

This generalises the C++ idiom of tying resource lifetime to object lifetime, with the single-owner rule removing the ways that idiom can go wrong: no dangling handle, no double close, no use after close.

It also explains a pattern that looks odd at first. A mutex guard in Rust is a value whose scope is the critical section, and the lock is released when it drops. Forgetting to unlock is not a bug you can write, because unlocking is not something you do.

7. What this costs

The trade is real and worth stating plainly, because the enthusiasm around Rust often skips it.

Some correct programs are rejected. The checker is conservative: it proves safety, so anything it cannot prove is refused. A program you know is fine may not compile, and the lesson What Undecidability Costs Real Tools explains why this is structural rather than a gap to be closed.

Data structures with sharing are harder. A doubly linked list, a graph with back-edges, a tree with parent pointers: all natural in a garbage-collected language and all fighting the single-owner rule. They are written using the tools of the fourth lesson, and they are more work.

There is a learning cliff. The first weeks involve the compiler rejecting code for reasons that are correct and unfamiliar. This is a genuine cost, paid once.

What you get in exchange is a specific and checkable list: no use after free, no double free, no null dereference, no data races, and no runtime or pause to achieve it.

Whether that trade is worth it depends entirely on whether those failure modes matter for what you are building, which is the same question the verification cursus asked about proofs.

8. The three models compared

ManualGarbage collectedOwnership
When freeing is decidedBy the programmerAt runtime, by a collectorAt compile time
Runtime costNoneCollector, pauses, overheadNone
Use after freePossibleImpossibleRejected at compile time
LeaksPossibleMostly preventedPossible, and safe
Cyclic structuresEasy, and error-proneEasyAwkward, needs extra tools
Cost locationDebuggingRuntimeCompile time and learning

The fourth row is the honest one. Rust prevents unsafety, not waste. A reference cycle can still leak, and leaking is considered safe in Rust's sense: it wastes memory without corrupting anything. That surprises people who assume safe means correct.

The last row is the summary. All three models pay; they differ in when. Manual pays during debugging, garbage collection pays during execution, ownership pays during compilation and during the first month of learning.

What the rule cannot yet express is temporary access. Moving a value to read it and moving it back is unusable, so the model needs a way to lend a value without transferring ownership. That is borrowing, and it is where the compiler starts reasoning about time.

Check your understanding

The lesson ends with a 5-question quiz. Take it in the player above to see your score.

  1. Why does the single-owner rule make compile-time freeing possible?
    • Because the compiler knows exactly one place where each value dies, without needing to know what else refers to it
    • Because all values are allocated on the stack
    • Because the runtime tracks reference counts
    • Because values are freed in the order they were created
  2. After `let b = a;` where `a` is a String, why is `a` unusable?
    • Because the heap buffer was copied and `a`'s copy was discarded
    • Because ownership moved, and letting both own it would mean both scopes free it: a double free
    • Because Strings are immutable once assigned
    • Because `a` was allocated on the stack and went out of scope
  3. What is copied when a String is moved?
    • The entire heap buffer
    • Nothing; the variable is aliased
    • The pointer, length and capacity, while the heap buffer is untouched
    • A reference count is incremented
  4. Why is `clone` an explicit, named call rather than implicit?
    • Because cloning can fail and must be handled
    • Because it requires a runtime allocator hook
    • Because implicit copy constructors would violate the borrow checker
    • Because it makes every heap duplication visible in the source and greppable when profiling
  5. Which failure does Rust's ownership model NOT prevent?
    • Memory leaks, for example through a reference cycle
    • Use after free
    • Double free
    • Freeing a resource twice via a file handle

Related lessons

Programming
advanced

Refs: A Branch Is a File Containing a Hash

Hashes are unusable by hand, so Git names them. A branch is not a copy of anything, not a directory, and not a container for commits: it is a forty-character file. Once that is clear, checkout, reset, detached HEAD and the reflog stop being separate concepts and become one operation on one kind of file.

8 steps·~12 min
Programming
advanced

Scheduling and Resources: Requests Are Not Limits

Two numbers govern where a pod lands and how it behaves under pressure, and they do completely different jobs. Requests are used for placement and are a promise; limits are enforced at run time and are a ceiling. This lesson separates them, covers the asymmetry between CPU and memory enforcement, and explains what actually happens when a node runs out.

8 steps·~12 min
Programming
advanced

The Runtime Stack, and What Isolation Is Worth

One command hides four layers of software and a set of standards that made them interchangeable. This lesson takes the stack apart, then asks the question the whole path has been building toward: given a shared kernel, how much is container isolation actually worth, and what has to be added before it is a security boundary.

8 steps·~12 min
Programming
advanced

Networking and Storage: Connecting an Isolated Thing

Isolation is the point, and a container that can reach nothing and keep nothing is useless. This lesson covers how a network namespace is wired to the outside, why published ports and container-to-container traffic work completely differently, and where data has to live given that the writable layer disappears.

8 steps·~12 min