Who frees the memory?
Every value your program creates has to be cleaned up eventually. A string, a buffer, a connection: at some point the memory it sits on must go back to the system, and something has to decide when. This is not a Rust question. It is a question every language answers, and the answer it picks shapes how the language feels to write. You have already lived with two of those answers, probably without naming them.
The first answer is a garbage collector. In Go, Java, Python, and JavaScript, you allocate freely and never write a line to release anything. A runtime watches your values, notices when nothing can reach them anymore, and reclaims them later. This is wonderfully convenient: you think about your problem, not about memory. The cost is that the collector is a program running alongside yours. It uses memory to track your memory, and every so often it does work you did not ask for, sometimes pausing your code mid-flight to catch up. In a web handler you rarely feel it. In a tight loop, an audio thread, or a system with a hard latency budget, those pauses are exactly the thing you were trying to avoid.
The second answer is to free by hand. In C and C++, you allocate, and you are responsible for releasing. There is no runtime overhead and no surprise pause, because nothing happens that you did not write. The cost moves onto you. Forget to free, and the value leaks; the program slowly bloats. Free a value while another part of the code still points at it, and you get a use-after-free: the memory has been handed back, but your code reads it anyway. Free the same value twice, a double-free, and you corrupt the allocator’s own bookkeeping. These are not exotic mistakes. They are among the most common and most dangerous bugs in systems software, and the compiler that accepted your code said nothing.
So you have a convenient answer that costs you a runtime, and a fast answer that costs you safety. For a long time those looked like the only two seats at the table. Rust takes a third. The rule, stated plainly and without any mechanics yet, is this: every value has exactly one owner, and when that owner goes away, the value is cleaned up right then, automatically. No collector watches in the background. You write no free and no defer. Cleanup is tied to a place in your code you can point at, so it is as automatic as a garbage collector yet costs nothing at runtime like manual free.
Here is that idea as a program. Read it for shape, then press Run. Notice what is not in it: no allocation call you have to match with a release, no defer cleanup() the way you might write in Go, no mention of a collector. There is just a value, a scope, and a closing brace.
// One owner, automatic cleanup. There is no free call and no defer here.
fn main() {
// `greeting` owns this String. It is the sole owner of that heap buffer.
let greeting = String::from("hello");
println!("greeting: {greeting}");
// No collector runs, and you write no cleanup line. When `greeting` goes
// out of scope at the closing brace below, Rust frees the String for you.
println!("the String is freed at the closing brace, automatically");
} // <- here: `greeting` is dropped and its memory is released, deterministically.The String named greetingowns a buffer on the heap. While the function runs, that buffer is its responsibility and no one else’s. At the closing brace of main, greeting goes out of scope, and Rust releases the buffer on the spot. The freeing is real and it is deterministic, meaning it happens at a known point you can put your finger on, not whenever a collector next decides to sweep. You did not write the cleanup, and you also did not pay a runtime to perform it. That is the whole promise of this chapter, and the rest of it is about how one small word, owner, makes the promise hold.
Every language has to decide when a value gets cleaned up, and there are three common answers. Go and Java use a garbage collector: convenient, since you never free anything by hand, but it adds a runtime that costs memory and can pause your program. C and C++ make you free by hand: fast, with no runtime overhead, but forgetting to free leaks memory and freeing at the wrong time gives you use-after-free and double-free bugs. Rust ties cleanup to ownership: it is automatic like a collector, yet has no runtime cost like manual free.
Every value has exactly one owner
Here is the rule the whole chapter turns on, in one sentence. Every value in Rust has exactly one owner: a single binding that is responsible for the value, and when that binding’s scope ends, the value is dropped. An owner is just the name that holds a value and answers for its cleanup. A scope is the region of code where that name is alive, almost always a pair of braces. There is no second owner, no shared bookkeeping, no reference count deciding when to free. One value, one owner, and a clear moment when that owner disappears.
Think about what a value made of String::from("data") actually is. The binding lives on the stack, a small fixed slot the function reserves. The text itself lives on the heap, a separately allocated buffer that can grow. The stack binding owns the heap buffer: it is the one thing tasked with releasing that memory. The figure draws exactly this, a single owner box on the stack pointing at the value it owns on the heap, and nothing else pointing anywhere.
String. When the binding’s scope ends, the value is dropped: its memory is released, with no GC and no manual free().When the owner’s scope ends, Rust runs cleanup automatically. There is no garbage collector pausing your program to find unreachable objects, and there is no free() you have to remember to call. The release happens at a moment you can point to in the source: the closing brace of the scope that holds the owner. This is the single most important difference to feel right now, so the snippet below makes it visible. A nested block creates a String, uses it, and then the block ends.
// One value, one owner. A nested block owns a String; when the block ends,
// the owner is gone and Rust drops the String for you. No free(), no GC.
fn main() {
// The inner braces are a scope: a region of code where bindings live.
{
// `data` is the owner of this String. It answers for its cleanup.
let data = String::from("data");
println!("inside the block: {data}");
// The block ends on the next line. `data` goes out of scope here, so
// Rust drops the String it owned: its heap memory is released.
}
// We are now past the block. There is no binding named `data` anymore, and
// the String it owned is already cleaned up. Outer code runs as normal.
println!("the block ended; that String is already gone");
}Read it for the timing, not the syntax. The String is created and printed inside the inner braces. The moment those braces close, the owner data is gone, and the String it owned is dropped on the spot: its heap buffer is handed back. The next println! runs in the outer scope, where no binding named data exists at all. Notice what governs the cleanup. It is not a function call returning, and it is not the program ending. It is the scope closing. That is the whole mechanism.
If you come from Go, this lands in a familiar but inverted place. In Go a local value also stops being reachable when it leaves scope, but the actual freeing is the garbage collector’s job, on the garbage collector’s schedule, sometime later. Rust moves that decision to compile time and ties it to scope, so the free happens deterministically at the closing brace, every run, in the same place. You give up the GC’s freedom to ignore lifetimes; you gain a release point you can predict by reading the braces.
Everything else in this chapter is a refinement of this one picture. Moving a value hands ownership to a new binding. Borrowing lets other code look at a value without taking ownership. Both only make sense once the base case is solid: by default, a value has one owner, and it lives exactly as long as that owner’s scope.
An owneris the binding responsible for a value’s cleanup, the one name that answers for it. A scope is the region of code, usually a pair of braces, where a binding is alive. When the scope ends, the owner is gone, and Rust drops the value it owned, releasing its memory and any other resources it held.
Assignment moves, it does not copy
This is the pivot of the chapter. In Go, Python, and Java, assigning a reference type to a new variable gives you a second handle to the same value, and both names keep working. Rust does something different for a value that owns a heap allocation: assignment moves ownership. The new binding owns the value, and the old binding is no longer usable. Best to feel it by breaking it on purpose. Here is the move that does not compile, with the real compiler output beneath it:
error[E0382]: borrow of moved value: `a`
--> use_after_move.rs:4:14
|
2 | let a = String::from("hi");
| - move occurs because `a` has type `String`, which does not
| implement the `Copy` trait
3 | let b = a;
| - value moved here
4 | println!("{a}");
| ^^^ value borrowed here after moveRead the compiler’s story top to bottom. let a = String::from("hi") makes a the owner of a heap buffer. let b = a; moves that ownership into b: the value left a, so a now owns nothing. The final line tries to read a anyway, and the compiler stops you cold. The fix is to reach for the value through its new, valid owner, b. Run the corrected version:
fn main() {
// A String owns a buffer on the heap. It is not Copy, so assigning it
// moves ownership rather than duplicating the buffer.
let a = String::from("hi");
// This line MOVES ownership of the buffer from a into b.
// After it, a owns nothing and is statically dead: reading a is a
// compile error (error[E0382]: borrow of moved value: `a`).
let b = a;
// So reach for the value through its new, valid owner.
println!("{b}");
}let b = a;, the heap String has exactly one owner: b. The dashed arrow is ownership leaving a, which now owns nothing and cannot be read.In Go, b := a gives you a second handle to the same slice header (or a fresh copy of a struct), and a keeps working either way. In Rust, let b = a; for a heap-owning type like String moves ownership: a is statically dead, and touching it is a compile error, not a runtime surprise. The check happens at compile time, before the program ever runs, so there is no dangling read to discover in production.
Why move, and not two owners
The move rule can feel arbitrary the first time you meet it. Why should let b = a poison the name a? Go never does this; you alias a slice or a map header all day and nobody complains. The answer is not about syntax preference. It is about who runs the cleanup, and how Rust guarantees that cleanup happens exactly once. Section 01 promised automatic cleanup with no garbage collector and no manual free. The move rule is the mechanism that keeps that promise honest.
Picture what would happen if two owners were allowed. Suppose a and b both owned the same heap value, the way two pointers in C can point at one allocation. Each owner is responsible for cleaning up when it goes out of scope, so when the block ends, a’s closing brace frees the value, and then b’s closing brace frees the same value a second time. That is the classic double-free bug: the second free hands the allocator a chunk it already reclaimed, and you get corruption or a crash, often far away from the line that caused it. Allowing two owners is exactly what invites this disaster.
The move rule removes the possibility by construction. It does not detect double-frees and warn you about them; it arranges the world so the second owner never exists. When the value moves from a to b, a stops owning it. There is always exactly one owner standing, so there is always exactly one closing brace that will do the freeing. One owner, one free. The bug is not caught at runtime, it is ruled out before the program ever runs.
This is the trade Rust is making, and it is worth stating plainly. Go reaches the same safety a different way: a garbage collector watches every reference and frees the value only after the last one is gone, so you can alias freely and never think about double-frees. Rust refuses the collector and its pauses, and pays for that refusal with the move rule. You give up casual aliasing of owned values; in return you get deterministic cleanup at a known point (the closing brace) with zero runtime bookkeeping. The discipline you feel from the borrow checker later is the price of this bargain.
The same rule covers passing a value to a function, which is where you will meet it most. Read the program below. consume takes a String by value, prints it, and returns. Handing payload to consume moves it in, exactly the way let b = a moves between names. consume is now the owner, so the value is cleaned up at consume’s closing brace, not main’s. Back in main, the name payload owns nothing; the commented-out line shows what trying to use it would cost you, a compile error rather than a use-after-free at runtime.
// consume takes ownership of the String it is handed. When consume returns,
// the value goes out of scope here and is cleaned up: exactly one owner did
// exactly one freeing.
fn consume(s: String) {
println!("consume got: {s}");
// s is dropped at this closing brace, because consume owns it now.
}
fn main() {
let payload = String::from("payload");
// Passing payload to consume moves it, just like `let b = a` would.
// After this line the name `payload` no longer owns anything.
consume(payload);
// Using payload here would not compile: the value left main with consume.
// println!("{payload}"); // error[E0382]: borrow of moved value: `payload`
println!("back in main; the String left with consume and is already cleaned up");
}Notice what the program does not contain: no free, no defer, no destructor you wrote by hand, and no collector running in the background. The cleanup is implied by ownership and happens the instant the single owner goes out of scope. That is the whole payoff of the picture. Once you accept that a value has one owner at a time, automatic and exact cleanup falls out for free, and the double-free in the left half of the figure simply cannot be drawn in real Rust.
Calling consume(s) moves s into the function in exactly the way let b = a moved between names. The function becomes the owner, it drops the value when it returns, and the caller can no longer use the name it gave away. Functions take ownership of their arguments unless you say otherwise, and saying otherwise (lending instead of giving) is the whole story of the next chapter.
Small values are copied, not moved
The model so far has one honest gap. You have watched a value move from one binding to another, leaving the first binding empty, and you saw the compiler refuse to let you touch the value you gave away. Taken too far, that turns into a wrong belief: that every assignment in Rust hands ownership over and leaves a hole behind. It does not. Plain numbers do not behave that way at all. Assign an i32 to a second binding and you get two independent integers, both fully usable, with nothing taken from anyone.
The dividing line is about what the value is, not how you use it. Some values are entirely a small, fixed block of bytes with nothing attached to clean up: an i32, an f64, a bool, a char, and tuples or arrays built only from those. Duplicating such a value is just duplicating its bytes, and the duplicate is a complete, harmless copy that owes nothing to the original. Rust calls these types Copy, and for them assignment makes a copy and leaves both bindings alive. A String, a Vec, or anything that owns a heap allocation is the opposite case: its bytes are a small handle pointing at a resource elsewhere, so copying the bytes blindly would give you two owners of one allocation. That is exactly the double-free the previous sections ruled out, so those types are deliberately not Copy, and assignment moves them instead.
Run this to see both behaviors next to each other. The integer copies and both names print; the String moves, and only the new owner is used afterward:
// Some values are copied on assignment; others move. The difference is
// whether the value is a small, self-contained block of bytes (Copy) or
// owns a resource like a heap allocation (move).
fn main() {
// i32 is Copy. Assigning x to y duplicates the bytes, so both stay usable.
let x: i32 = 7;
let y = x; // a copy: x is NOT consumed here
println!("x is {x} and y is {y} (i32 was copied)");
// String owns a heap allocation, so it is NOT Copy. Assigning moves it:
// ownership leaves s and lives in t. After this, s is gone; only t prints.
let s = String::from("the String now lives in t (it moved out of s)");
let t = s; // a move: s is consumed, t is the new owner
println!("{t}");
// println!("{s}"); // would not compile: s was moved out of
}Read the two halves against each other. let y = x; copies the seven, so the next line prints x and y with no complaint, because the copy did not consume x. let t = s; moves the string, so s is now empty and the commented-out line that tries to print it would not compile. Same syntax, two outcomes, and the type alone decides which one you get. There is no special operator and no annotation at the call site; x is Copy and s is not, and the compiler reads that off the types.
This also closes a loop from the move error in section 03. When the compiler rejected the use-after-move there, its note said the value does not implement the Copy trait. Now that phrase has a meaning: the compiler moved the value precisely because its type was not Copy, and it was telling you that copying was not an option it could fall back on. For a Copy type the same code would have compiled, because the assignment would have left the original intact. The error was never about the syntax you wrote; it was about which side of this line your type sits on.
If you come from Go, the copy half will feel ordinary and the move half is the new part. In Go, assigning an int copies it and assigning a slice or a map copies a small header that still points at the same backing array, so two variables can quietly share and mutate one buffer. Rust agrees with Go on the plain int: that is the Copy case. Where it diverges is the shared-handle case. Rather than let two bindings point at one allocation, Rust moves ownership to exactly one of them, which is why String assignment empties the source instead of aliasing it.
A type is Copy only when duplicating its bytes is a complete and harmless copy with nothing left to clean up: integers, floats, bool, char, and tuples or arrays built entirely from those. A String, a Vec, or anything that owns a heap allocation is deliberately not Copy, because two owners of one allocation is exactly the double-free Rust rules out. So those values move on assignment, while the small self-contained ones copy. When you are unsure which a type is, the rule of thumb is simple: if it owns a resource, it moves.
When you really do want two: clone
The model so far can feel like a cage. One value, one owner, and handing it to someone else takes it away from you. But sometimes you genuinely want two: two independent strings that two parts of your program can each keep and change without disturbing the other. Rust does not forbid that. It just makes you ask for it by name. The name is clone.
Here is the whole move. You start with one owned String, and you call .clone() on it to produce a second one. That call does real work: it allocates a fresh piece of memory on the heap and copies the bytes into it. When it returns, you have two completely separate strings. The original binding still owns its value, the new binding owns the copy, and nothing was taken from anyone. Both are usable for the rest of the function.
// Two independent owners, on purpose. clone makes a real second String:
// a fresh heap allocation that holds its own copy of the bytes.
fn main() {
let a = String::from("data");
// clone allocates a brand-new String and copies a's bytes into it.
// a keeps its value; b owns a separate one. Two owners, two allocations.
let b = a.clone();
// Both bindings are usable now, because neither moved into the other.
println!("a is {a} and b is {b}");
println!("both are usable because clone made a second, independent String");
}Notice what changed versus a plain assignment. Earlier, writing let b = a; moved the one value from a into b and left a empty, because there was still only one value and ownership of it had to land in exactly one place. Writing let b = a.clone(); creates a brand-new value first, so now there are two values, one for each binding. The cage was never about the syntax of assignment; it was about how many values exist. clone makes a second one.
The honest part is the cost. A clone is an allocation and a byte-for-byte copy, and for a big string or a large collection that is real work you are paying every time the line runs. This is exactly why Rust will not do it for you silently. A garbage-collected language like Go is happy to copy and share values behind the scenes; you rarely know the moment an allocation happened, because the runtime smooths it over and the collector cleans up later. Rust takes the opposite stance: if a copy was paid for, you can see the word clone sitting right there in the code that paid for it.
So clone is your escape hatch, not your default. Reach for it when you truly need two independent owned values that will go separate ways. Very often, though, you do not need a second owner at all. You just need a part of your program to look at the value for a moment without taking it. That cheaper move, borrowing instead of copying, is what the next chapter is about.
A garbage-collected language hides copies and sharing behind the scenes. You rarely know the exact moment an allocation happened, because the runtime handles it and the collector tidies up afterward. Rust makes the duplication visible instead: if you see clone, you know a real copy was paid for right there on that line. And often you do not actually need a second owner at all, just a temporary look at the value. That cheaper move is the subject of the next chapter.
Ownership you can already see in Go
You have done ownership before. Every time you wrote a Go comment like // caller must Close this, or paused before a defer file.Close() to make sure nothing else still held the handle, or asked yourself whether it was safe to read a slice after you passed it to a goroutine, you were answering an ownership question. Who is responsible for this value, and is it still mine to touch? Go gives you no language feature for that. It gives you convention, documentation, and code review. You answer the question every day; you just answer it in your head.
Look at what this small program does, then press Run. It makes two values, uses them, and never frees anything by hand. There is no Close, no defer, no manual cleanup. Watch the order of the last two lines:
// A tiny type that announces when it is cleaned up. The Drop impl prints a line
// the instant the value goes out of scope; we use it only to make ownership-driven
// cleanup visible. (Drop is a trait for end-of-life cleanup; it gets its own
// chapter later. For now, just watch when the lines appear.)
struct Guard {
name: &'static str,
}
impl Drop for Guard {
fn drop(&mut self) {
println!("dropping {}", self.name);
}
}
fn main() {
// Two values, owned by this block, created in order.
let first = Guard { name: "first" };
let second = Guard { name: "second" };
// Use them while they are alive.
println!("using {}", first.name);
println!("using {}", second.name);
// No free, no close, no defer. At the closing brace the block drops what it
// owns, in reverse order of creation: last in, first out.
}The two values are owned by the block they live in. When execution reaches the closing brace, the block cleans up what it owns, and it does so in reverse order of creation: second goes before first, last in, first out. You did not schedule that. You did not write a deferfor each one and trust yourself to get the order right. Ownership decided it. Because the compiler knows exactly which scope owns each value, it knows exactly when each value’s life ends, and it inserts the cleanup for you. In Go the same discipline is real but informal: you reach for defer, you reason about which call closes the handle, and you hope the next reader keeps the contract. Here the contract is the value’s scope, and it is not optional.
The printed lines are produced by a small piece of cleanup code that runs the instant a value goes out of scope. The mechanism has a name, Drop, and it gets a full chapter later; for now it is just a window. It lets you see the moment ownership ends, which is normally silent. The point is not the trait. The point is that cleanup here is deterministic and ownership-driven, not handed to a garbage collector that runs whenever it likes. The value’s owner is known, so the value’s end is known.
Now reframe the thing you have heard people complain about. When a Rust programmer says they are fighting the borrow checker, what is usually happening is this: they wrote code that does not answer the ownership question, and the compiler refused to guess. Is this value still yours to use after you handed it off? In Go that question is settled by a comment and a reviewer’s attention. In Rust it is settled by the type of the value, at compile time, before the program ever runs. The compiler is not inventing a new rule to obstruct you. It is asking, out loud, the same question you were always supposed to answer, and declining to continue until you do. You have now met the first half of that conversation: who owns this, and when does its life end.
Who owns this and is it safe to use after I hand it off are questions you answer in every language. In Go they live in comments and code review; in C they live in your head, and a mistake is a use-after-free. Rust moves them into the type of the value, so the compiler answers them alongside you. That is what people mean by the borrow checker, and you have met its first half here: ownership decides who is responsible for a value and when its life ends.
The shape of ownership
This chapter had one job: to put ownership in your hands as a picture, not a rulebook. So read the model once more in a single breath. Every value has exactly one owner at a time. Plain assignment and ordinary function calls move that ownership from one name to another, and the name you moved out of goes quiet: it still exists, but it owns nothing, so the compiler will not let you touch it. Small self-contained values are the exception: a number, a bool, a char are Copy, so assigning one duplicates the bits and leaves the original perfectly alive. When you genuinely need a second living owner of something larger, clonebuys you one by making a real, separate copy of the data. And cleanup is not a chore you schedule or a pause you wait on: the moment an owner’s scope ends, its value is freed, deterministically, right there.
Here is that whole sentence as a program. It builds a String and owns it, clones a second independent owner, hands one copy to a function that consumes it, and goes on using the other; an i32 rides alongside to show a Copy value surviving an assignment untouched. Each line prints what is still alive, so you can watch the model play out in order. Read it, predict the trace, then run it:
// One pass through the whole model: own a value, clone a second owner,
// hand one copy to a function that consumes it, and keep the other.
// An i32 rides alongside to show a small Copy value surviving assignment.
// `consume` takes the String by value, so it OWNS the argument. When this
// function returns, its parameter goes out of scope and the String is freed.
fn consume(s: String) {
println!("consumed one copy: {s}");
}
fn main() {
// 1. Build a String. `report` is its one owner.
let report = String::from("report");
println!("owner built: {report}");
// 2. clone() allocates a fresh String with the same bytes. Now there are
// two genuinely independent owners, `report` and `backup`.
let backup = report.clone();
println!("second owner via clone: {backup}");
// 3. Move `report` into consume(). After this line `report` is gone:
// ownership left main, and the value is freed when consume() returns.
consume(report);
// 4. `backup` was never moved, so it is still alive and usable here.
println!("still holding the other: {backup}");
// 5. An i32 is Copy: assigning it duplicates the bits, so BOTH names
// stay valid. No move, no clone, nothing to free.
let counter: i32 = 1;
let mirror = counter;
println!("the counter copied, both alive: {counter} and {mirror}");
}Trace the lifecycle against the output. report is built and owned. backup is a real second owner courtesy of clone, not a shared view of the first. The call consume(report) moves report into the function; when consume returns, that String is freed, and any later use of report would be a compile error, not a runtime surprise. But backup was never moved, so the next line uses it without ceremony. The counter is Copy, so let mirror = counter leaves both names valid and there is nothing to free. If you came from Go, this is the part that feels unfamiliar: there is no garbage collector deciding later, no defer you had to remember to write. Scope ends, the value drops, every time.
Be honest with yourself about what this bought you, because Part II trades in intuition, not fluency. You can now read a snippet and call the moves: you can point at the line where a value becomes unusable, the line where it is merely copied, and the exact moment cleanup happens. What you cannot yet do is sit in front of the compiler and satisfy it from memory, and you are not supposed to. That skill, the borrow checker as a discipline you internalize rather than a picture you read, is the work of Part III. For now, reasoning about ownership in pictures is the entire goal, and you have it.
One picture is still missing, and it is the one you reach for most. Most of the time you do not want to give a value away, and you do not want to pay for a clone just to read it for a moment. In Go you would pass a pointer and not think twice. Rust has its own answer, and it is the centerpiece of the next chapter: borrowing, the act of looking at a value without taking ownership of it. Once you can borrow, moves and clones stop being the only two options, and the model you just learned snaps into its real shape.
You can now read move semantics in a snippet and predict the outcome: when a value becomes unusable because it moved, when it is merely copied, and the moment cleanup runs. What you cannot yet do is satisfy the borrow checker from memory, and you are not meant to: that is Part III. For now you can reason about ownership in pictures, which is exactly the goal of this chapter.
The whole chapter in seven lines
- Every value has exactly one owner, and when that owner’s scope ends, Rust drops the value automatically, with no garbage collector and no manual free.
- Assignment and function calls move ownership: the new binding owns the value and the old name becomes unusable, caught at compile time, not at runtime.
- The move rule exists to make double-free impossible by construction, since there is always exactly one owner to do the freeing.
- Small self-contained values (integers, floats,
bool,char) are Copy: they duplicate cheaply and both bindings stay usable. clonebuys a genuine second owner with a real allocation, made explicit on purpose so you always know when a copy was paid for.- Cleanup is deterministic: values drop in reverse order of creation the moment their owner’s scope closes.
- This chapter built the picture, not borrow-checker fluency; that is Part III, and looking at a value without taking it is the next chapter.