Language Internals
Part 3 of 11 · Rust Language ProficiencyOwnership, Borrowing & Lifetimes
Ownership, moves, borrowing, lifetimes vs GC aliasing.
- 1Gist
- 2Maps
- 3Q&A
- 4Sandbox
Voice readout needs Web Speech Synthesis in this browser.
Question ladder
L1
Who frees a Rust value?
Answer
Drop, when the owner leaves scope. There is no later collector for that owner.
L2
Why does String move while i32 copies?
Answer
String owns a heap buffer and is not Copy. i32 is Copy, so assignment duplicates the bits.
L3
What is a shared borrow?
Answer
A shared reference. Many can exist. None of them may be used to mutate while they are live.
L4
What is an exclusive borrow?
Answer
A mutable reference. It must be the only borrow of that value for its lifetime.
L5
When do you write a lifetime parameter?
Answer
When a struct or a function signature stores or returns a reference whose source is not obvious.
L6
Are lifetimes present at runtime?
Answer
No. They are erased after the check, the way TypeScript types are erased, except they guarded memory.
L7
When is clone the honest fix?
Answer
When two owners of equal data are required. It is not the fix for a borrow that could have been shorter.
Failure modes
Use after move
The source name is used after ownership moved into a call or another binding.
Overlapping borrows
A shared borrow is still live when code asks for an exclusive borrow.
Dangling struct field
A struct holds a reference and the owner dies first. The lifetime parameter makes that a compile error.
Clone as a reflex
Every call clones, so the hot path allocates and the API hides who should own the data.
Misconceptions
A lifetime extends an object's life.
It names a bound the checker enforces. It does not keep memory alive the way a GC root does.
The borrow checker is a lock.
It is a compile-time proof. RefCell and Mutex are the runtime tools, on a later page.
Everything should be passed by exclusive borrow.
Read-only APIs take a shared borrow. Exclusive is for mutation.
Interviewer traps
Explaining a move as a deep copy.
A move transfers ownership. Copy duplicates bits. clone is the explicit deep copy.
Saying Python lists move on assignment.
Python aliases. Mutation is visible through every name. Rust does not.
Design scenario
Same prompt for every reader.
Requirements
The caller keeps the String. The function returns a shared borrow tied to that buffer, not a new allocation, unless the caller asks for an owned String.
Traffic / scale
One buffer, many short-lived views.
Latency
The view must not copy the bytes.
Consistency
The view cannot outlive the buffer.
Availability
A use-after-free is a compile error, not a production crash.
Failure assumptions
- The function returns a reference and the caller frees the buffer first.
- Assignment of the String is treated as an alias.
Constraints
- Stay on ownership. Do not introduce Rc yet.
Prompt
A parser returns the first word of a buffer the caller still owns.
Move versus alias
Prefer
One owner
Non-Copy assignment transfers the value. Borrows are temporary badges.
- The source name dies on move.
- Readers do not overlap a writer.
- Drop runs at the end of the owner.
Alternative
Many aliases
TypeScript and Python assignment shares the object. Mutation shows through every name.
- The source name stays valid.
- Two writers are a runtime problem.
- Collection waits until the object is unreachable.
Overview
This is the hardest chapter if you learned on a garbage collector. One owner. Moves by default. A shared borrow or an exclusive borrow, not both. Lifetimes name how long a borrow is valid.
The JS/TS language hub and the Python language hub describe heaps that alias by default. Use them as the contrast, then come back to the move.
Comparative
| Pattern | TypeScript | Python | Rust |
|---|---|---|---|
| Assign an object | alias | alias | move, unless Copy |
| Many readers | default | default | shared borrow |
| Mutate while shared | default | default | compile error |
Flow
- 1
Step 1 let mut s = String::from - s is the owner
- nextStep 2 let r1 = &s - shared borrow
- 2
Step 2 let r1 = &s - shared borrow
- nextStep 3 let r2 = &s - more readers are fine
- 3
Step 3 let r2 = &s - more readers are fine
- nextStep 4 Last use of r1 and r2 - borrows end (NLL)
- &mut s while r1 is used laterFailure path - E0502 cannot borrow as mutable
- 4
Step 4 Last use of r1 and r2 - borrows end (NLL)
- nextStep 5 let w = &mut s - exclusive borrow now allowed
- 5
Step 5 let w = &mut s - exclusive borrow now allowed
- nextStep 6 s dropped at the end of scope
- 6
Step 6 s dropped at the end of scope
- 7
Failure path - E0502 cannot borrow as mutable
Lesson map
Ownership, Borrowing & Lifetimes
Ownership, moves, borrowing, lifetimes vs GC aliasing.
Architecture. Step 1 let mut s = String::from - s is the owner Ready. Step 2 let r1 = &s - shared borrow Ready. Step 3 let r2 = &s - more readers are fine Ready. Step 4 Last use of r1 and r2 - borrows end (NLL) Ready. Step 5 let w = &mut s - exclusive borrow now allowed Ready. Step 6 s dropped at the end of scope Ready. Failure path - E0502 cannot borrow as mutable Ready
Select a node to see why it exists, or an edge to see the protocol, direction, effect, and consequence.
Mermaid export
flowchart TB S["Step 1 let mut s = String::from - s is the owner Ready"] R1["Step 2 let r1 = &s - shared borrow Ready"] R2["Step 3 let r2 = &s - more readers are fine Ready"] U["Step 4 Last use of r1 and r2 - borrows end (NLL) Ready"] W["Step 5 let w = &mut s - exclusive borrow now allowed Ready"] D["Step 6 s dropped at the end of scope Ready"] F["Failure path - E0502 cannot borrow as mutable Ready"] S -->|continues| R1 R1 -->|continues| R2 R2 -->|continues| U U -->|continues| W W -->|continues| D R2 -->|&mut s while r1 is used later| F
Press Run. Snippets must be self-contained — no network, files, or native modules.
Rosetta — move versus alias
fn main() {
let a = String::from("hi");
let b = a;
// println!("{a}"); // used after move
println!("{b}");
let x = 5i32;
let y = x;
println!("{x} {y}");
}let a = { text: "hi" };
let b = a;
a.text = "bye";
console.log(b.text);a = ["hi"]
b = a
a.append("!")
print(b)Rust will not let two names own one String. i32 is Copy, so both names work. TypeScript and Python share, and a write is visible through every alias. Call clone in Rust only when you need two equal owners.
Rosetta — shared versus exclusive
fn len_bytes(s: &str) -> usize {
s.len() // UTF-8 bytes, not Unicode scalars
}
fn push_bang(s: &mut String) {
s.push('!');
}
fn main() {
let mut name = String::from("Rust");
println!("{}", len_bytes(&name));
// let r1 = &name; let r2 = &mut name; println!("{r1}"); // ERROR: r1 still used after the &mut borrow
push_bang(&mut name);
println!("{name}");
}function lenBytes(s: string): number { return new TextEncoder().encode(s).length; }
function pushBang(s: { text: string }) {
s.text += "!";
}
const name = { text: "Rust" };
pushBang(name);
console.log(name.text);def len_bytes(s: str) -> int:
return len(s.encode("utf-8"))
def push_bang(s: list[str]) -> None:
s.append("!")
name = ["Rust"]
push_bang(name)
print(name)Rust rejects a live shared borrow overlapping an exclusive one. The commented line is E0502 because r1 is still used after the exclusive borrow. len_bytes counts UTF-8 bytes: TextEncoder is required because a TypeScript string length counts UTF-16 code units, and Python encodes first because len(s) counts characters. TypeScript and Python allow the overlap. Threads then need locks, which is a later page.
Rosetta — a struct that stores a borrow
struct Highlight<'a> {
text: &'a str,
}
fn first_word(s: &str) -> &str {
s.split_whitespace().next().unwrap_or("")
}
fn main() {
let s = String::from("hello world");
let h = Highlight { text: first_word(&s) };
println!("{}", h.text);
}type Highlight = { text: string };
function firstWord(s: string): string {
return s.split(/\s+/)[0] ?? "";
}
const h: Highlight = { text: firstWord("hello world") };
console.log(h.text);from dataclasses import dataclass
@dataclass
class Highlight:
text: str
def first_word(s: str) -> str:
parts = s.split()
return parts[0] if parts else ""
h = Highlight(text=first_word("hello world"))
print(h.text)The 'a on Highlight ties the borrow to the referent. TypeScript and Python copy or share a string under the collector and have no lifetime parameter. The lifetime is gone after compilation.
Interview Q&A
Why does String move but i32 copy?
Answer
String owns a heap buffer and is not Copy. i32 is Copy, so assignment duplicates the value.
Are lifetimes runtime?
Answer
No. They are compile-time only and erased, like TypeScript types, except they were checked against memory.
What does shared XOR exclusive mean?
Answer
Many shared borrows, or one exclusive borrow. The checker rejects a writer while a reader is still live.
When do you clone?
Answer
When the API truly needs two owners of equal data. Shorten the borrow first.
How is this different from a JavaScript object assignment?
Answer
The JavaScript assignment aliases. Both names stay valid and see later writes. A Rust move invalidates the source.
How is this different from a Python list assignment?
Answer
The Python name is another alias. append is visible through both names. Rust would move the Vec or lend a borrow.
What does Drop replace?
Answer
The free you would write by hand, and the collector pass you would wait for. It runs when the owner ends.
What should you master before traits and async?
Answer
Moves, the two borrow kinds, and the moment a lifetime parameter shows up on a struct.
Pitfalls
- Using a name after it was moved.
- Holding a shared borrow across a call that needs an exclusive borrow.
- Returning a reference to a local.
- Adding a lifetime to every function before the signature needs one.
- Cloning to silence the checker.
Take let b = a for a String and for an i32. Say which source is still usable, and what you would pass if a function only needs to read the text.