Historical Context Rust's ownership system was designed by Graydon Hoare at Mozilla Research (announced 2010, 1.0 in 2015). The key insight — memory safety enforced at compile time without a garbage collector — drew on Cyclone (1998) for region-based memory, MLton for lifetime tracking in functional languages, and Mozilla's own experience with C++ security vulnerabilities. The same affine-type mechanism for resource tracking appears in linear logic (Girard, 1987) and session types (Honda, 1993), connecting Rust's practical design to deep theoretical foundations.## The Ownership Rules
Rust’s memory management rests on three rules enforced at compile time:
Each value in Rust has a single owner . When the owner goes out of scope, the value is dropped (memory is freed). There can be zero or more immutable references (&T) OR exactly one mutable reference (&mut T) to a value at any point in its lifetime. These rules are checked by the borrow checker, which operates on MIR (Mid-level Intermediate Representation). The borrow checker does not exist at runtime — there is zero overhead for ownership Tracking in the compiled binary.
let s1 = String :: from ( "hello" );
let s2 = s1; // s1 is MOVED to s2 — s1 is no longer valid
// println!("{}", s1); // ERROR: value borrowed after move
println! ( "{}" , s2); // OK — s2 owns the data
The move is a compile-time transfer of ownership. No memory is copied — only the pointer, length, And capacity (24 bytes for String on 64-bit) are copied. The original binding is invalidated.
Types are divided into two categories based on whether assignment copies or moves:
Category Examples Behavior Copy typesi32``f64``bool``char``(i32, i32)``&TAssignment copies the value Move types String``Vec<T>``Box<T>``FileUser-defined structs (unless Copy)Assignment transfers ownership
A type implements Copy if and only if every bit pattern of its memory representation is a valid Value. This is why types containing heap pointers (like String) cannot be Copy — a bitwise copy Would create two owners of the same heap allocation.
let p1 = Point { x : 1.0 , y : 2.0 };
let p2 = p1; // p1 is COPIED — both p1 and p2 are valid
println! ( "{} {}" , p1 . x, p2 . y); // OK
Copy requires Clone and is a marker trait with no methods. The compiler automatically implements Copy for types where all fields are Copy.
Types that cannot be Copy:
Any type with a Drop implementation (destructor) Any type containing a heap pointer (String``Vec``Box) Any type containing a mutable reference (&mut T) Structs can be partially moved — individual fields can be moved out while other fields remain valid:
name : String :: from ( "Alice" ),
let name = person . name; // name is moved out of person
// println!("{:?}", person); // ERROR: person partially moved
println! ( "{}" , person . age); // OK — age is Copy, was never moved
After a partial move, the struct itself is no longer usable as a whole, but its Copy fields remain Accessible.
Function arguments are moved by default:
fn takes_ownership (s : String ) {
let s = String :: from ( "hello" );
// println!("{}", s); // ERROR: s was moved
To avoid the move, pass a reference:
let s = String :: from ( "hello" );
println! ( "{}" , s); // OK — s was borrowed, not moved
Functions transfer ownership to the caller via return values:
fn creates_ownership () -> String {
String :: from ( "hello" ) // ownership moves to caller
fn takes_and_gives (s : String ) -> String {
s // ownership moves back to caller
An immutable reference &T allows reading but not modifying the referenced data. You can create any Number of immutable references simultaneously:
let s = String :: from ( "hello" );
println! ( "{} {} {}" , r1, r2, r3); // OK — multiple immutable borrows
A mutable reference &mut T allows reading and modifying. Only one mutable reference can exist at a Time, and no immutable references can coexist with a mutable one:
let mut s = String :: from ( "hello" );
// let r2 = &mut s; // ERROR: cannot borrow as mutable more than once
This is the core rule that prevents data races at compile time. The NLL (Non-Lexical Lifetimes) Borrow checker understands that r1 is no longer in use after its last usage point, not just at the End of the lexical scope:
let mut s = String :: from ( "hello" );
let r1 = & s; // immutable borrow starts
println! ( "{}" , r1); // r1 used here
// r1's borrow ends here (NLL)
let r2 = &mut s; // OK — r1 is no longer in scope
The borrow checker guarantees that references always point to valid data. This is one of Rust’s most Important safety guarantees:
let s = String :: from ( "hello" );
& s // ERROR: s is created inside this function and will be dropped
} // the reference would point to freed memory
fn no_dangle () -> String {
let s = String :: from ( "hello" );
s // OK — ownership is transferred to the caller
The compiler error is: missing lifetime specifier — it is telling you that it cannot prove the Reference will outlive its referent.
At any given lifetime scope for a value:
┌──────────────────────────────────────┐
│ &T &T &T (many immutable) │ ✓
│ &mut T (one mutable) │ ✓
│ &mut T &mut T (multiple mutable) │ ✗
└──────────────────────────────────────┘
Lifetimes are Rust’s way of tracking how long a reference is valid. Every reference has a lifetime, But in most cases the compiler can infer it (lifetime elision rules). Explicit lifetime annotations Are needed when the compiler cannot determine the relationship between input and output lifetimes.
Lifetimes are denoted with a leading apostrophe. By convention, 'a is the first lifetime, 'b the Second, etc.
fn longest <' a >(x : & ' a str , y : & ' a str ) -> & ' a str {
The annotation <'a> says: “there exists some lifetime 'a such that both x and y live at Least as long as 'aAnd the return value also lives at least as long as 'a.” The caller gets to Choose what 'a is, constrained by the actual lifetimes of the arguments.
The compiler applies three rules to elide (omit) lifetime annotations. If after applying all three Rules, the compiler still cannot determine lifetimes, it errors.
Rule 1: Each parameter that is a reference gets its own lifetime parameter.
fn foo (x : & str ) → fn foo<' a >(x : & ' a str )
fn foo (x : & str , y : & str ) → fn foo<' a , ' b >(x : & ' a str , y : & ' b str )
Rule 2: If there is exactly one input lifetime parameter, that lifetime is assigned to all Output parameters.
fn foo (x : & str ) -> & str → fn foo<' a >(x : & ' a str ) -> & ' a str
Rule 3: If there are multiple input lifetime parameters but one of them is &self or &mut selfThe lifetime of self is assigned to all output parameters.
fn method ( & self , x : & str ) -> & str → fn method<' a , ' b >( & ' a self , x : & ' b str ) -> & ' a str
Lifetimes can have bounds, just like type parameters:
// 'b must outlive 'a — 'b is at least as long as 'a
fn print <' a , ' b : "a>(x: &''b str, y: &" a str ) {
This is useful when a struct holds a reference and you need to ensure the struct does not outlive The referent.
When a struct holds a reference, you must annotate its lifetime:
let novel = String :: from ( "Call me Ishmael. Some years ago..." );
let words = novel . as_str ();
let i = words . find ( '.' ) . unwrap ();
first_sentence = Excerpt { part : & words[ .. i] };
// Excerpt<'a> where 'a is the lifetime of words
// first_sentence is invalid here — words was dropped
Lifetimes in function signatures establish relationships between input and output references. The Compiler does not change the actual lifetimes — it only verifies that the constraints are satisfied.
// The returned reference lives as long as the shorter of the two inputs
fn longest <' a >(x : & ' a str , y : & ' a str ) -> & ' a str {
if x . len () > y . len () { x } else { y }
// The returned reference lives as long as x only
fn first <' a , ' b >(x : & ' a str , _y : & ' b str ) -> & ' a str {
'static means the reference lives for the entire duration of the program. All string literals have 'static lifetime:
let s : & ' static str = "hello" ; // embedded in the binary
Cannot infer a shorter lifetime, but adding `'static` constraints reduces the function's Flexibility. A function taking `&'static str` cannot accept locally-owned `String` references, only String literals and values explicitly annotated with `'static`.Lifetimes are covariant in their position. Given 'a: "b (a outlives b), &''a T is a subtype of &"b T. This means a longer-lived reference can be used where a shorter-lived one is expected.
For &mut TLifetimes are invariant — you cannot substitute a &'a mut T where a &'b mut T Is expected, even if 'a: "b. This prevents soundness issues with mutable aliasing.
fn mutate < '' a>(r : & "a mut i32) {
let r: &'static mut i32 = &mut x; // ERROR: expected 'static, got shorter lifetime
The borrow checker’s rule “either many immutable or one mutable” is strict. Sometimes you need to Mutate data through a shared reference. Interior mutability types provide this capability safely.
Cell<T> provides copy-based interior mutability for Copy types. It stores the value inline (no Heap allocation) and provides get() (returns a copy) and set() (replaces the value):
let counter = Cell :: new ( 0 );
counter . set (counter . get () + 1 );
assert_eq! (counter . get (), 1 );
Cell<T> does not allow borrowing the inner value — you can only copy it out or replace it. This Means there is no risk of creating a dangling reference to the interior.
RefCell<T> provides reference-based interior mutability for any type. It tracks borrows at runtime With a reference count and panics if the borrowing rules are violated:
let data = RefCell :: new ( vec! [ 1 , 2 , 3 ]);
let borrow1 = data . borrow (); // immutable borrow (Ref<Vec<i32>>)
let borrow2 = data . borrow (); // OK — multiple immutable borrows
// let borrow3 = data.borrow_mut(); // PANIC: already borrowed immutably
let borrow3 = data . borrow_mut (); // OK — all previous borrows dropped
Immutable borrow is active will panic. This trades compile-time safety for runtime flexibility. Use `try_borrow()` and `try_borrow_mut()` to get `Result` instead of panicking.Graph data structures where nodes need to reference each other (cycles prevent compile-time borrow checking).Mocking in tests where you need to mutate internal state through a shared reference.Builder patterns where the builder is shared across multiple configuration steps. neighbors : RefCell & lt; Vec & lt; & ' a Node & lt;' a & gt; & gt; & gt;,
let a = Node { value : 1 , neighbors : RefCell :: new ( vec! []) };
let b = Node { value : 2 , neighbors : RefCell :: new ( vec! []) };
a . neighbors . borrow_mut () . push ( & b);
b . neighbors . borrow_mut () . push ( & a);
UnsafeCell<T> is the primitive underlying both Cell and RefCell. It is the only type in Rust That allows safe code to obtain a mutable reference to its interior through a shared reference. Using UnsafeCell directly requires unsafe code and is the foundation for all interior mutability Abstractions.
use std :: cell :: UnsafeCell ;
unsafe impl Sync for Counter {} // YOU must verify this is safe
fn new (value : i32 ) -> Self {
Counter { value : UnsafeCell :: new (value) }
Behavior. Only do this if you can prove that mutation is properly synchronized (e.g., via atomics or Platform-specific memory barriers).Rc<T> enables multiple ownership of the same data via reference counting. It is single-threaded — The compiler will prevent you from sending an Rc across thread boundaries.
let a = Rc :: new ( String :: from ( "hello" ));
let b = Rc :: clone ( & a); // increments ref count (NOT a deep clone)
let c = Rc :: clone ( & a); // ref count is now 3
assert_eq! ( Rc :: strong_count ( & a), 3 );
println! ( "{}" , a); // OK — all three bindings are valid
When the last Rc is dropped, the inner value is deallocated. Rc is not Send or SyncSo the Compiler prevents sharing it between threads.
Rc<RefCell<T>> is the idiomatic pattern for shared mutable ownership in single-threaded contexts:
let data = Rc :: new ( RefCell :: new ( vec! [ 1 , 2 , 3 ]));
let data2 = Rc :: clone ( & data);
data . borrow_mut () . push ( 4 );
assert_eq! (data2 . borrow () . len (), 4 );
Arc<T> is the thread-safe equivalent of Rc<T>. It uses atomic operations for reference counting, Making it Send and Sync. Arc is the foundation of shared ownership in concurrent Rust.
let data = Arc :: new ( vec! [ 1 , 2 , 3 ]);
let data2 = Arc :: clone ( & data);
let handle = thread :: spawn ( move || {
println! ( "len: {}" , data2 . len ());
println! ( "len: {}" , data . len ());
Arc<Mutex<T>> is the standard pattern for shared mutable state across threads:
use std :: sync :: { Arc , Mutex };
let counter = Arc :: new ( Mutex :: new ( 0 ));
let mut handles = vec! [];
let counter = Arc :: clone ( & counter);
handles . push ( thread :: spawn ( move || {
let mut num = counter . lock () . unwrap ();
assert_eq! ( * counter . lock () . unwrap (), 10 );
Both Rc and Arc support weak references via Weak<T>. A Weak does not increment the strong Reference count and does not prevent the value from being dropped. This prevents reference cycles (which would cause memory leaks):
parent : RefCell < Weak < Node >>,
children : RefCell < Vec < Rc < Node >>>,
let parent = Rc :: new ( Node {
parent : RefCell :: new ( Weak :: new ()),
children : RefCell :: new ( vec! []),
let child = Rc :: new ( Node {
parent : RefCell :: new ( Rc :: downgrade ( & parent)),
children : RefCell :: new ( vec! []),
parent . children . borrow_mut () . push ( Rc :: clone ( & child));
// When parent and child go out of scope, both are dropped.
// The Weak reference in child.parent does not prevent this.
Loops are a common source of borrow checker errors. The key issue is that the borrow must not Outlive the value being borrowed:
let mut v = vec! [ 1 , 2 , 3 , 4 , 5 ];
// This works — we borrow the vector immutably for the iteration
// This works — we borrow the vector mutably, but each item is borrowed
// for a single iteration
let mut v = vec! [ String :: from ( "a" ), String :: from ( "b" )];
// This pattern is tricky:
v . push ( String :: from ( "c" )); // ERROR: immutable borrow of v (first) conflicts
// with mutable borrow (push)
The problem: push may reallocate the vector’s buffer, invalidating all existing references. The Borrow checker conservatively rejects this because it cannot prove (at compile time) that push Will not reallocate.
// Fix 1: Drop the borrow before mutation
let mut v = vec! [ String :: from ( "a" ), String :: from ( "b" )];
let first = v[ 0 ] . clone ();
v . push ( String :: from ( "c" ));
// Fix 2: Use indices instead of references
let mut v = vec! [ 1 , 2 , 3 , 4 , 5 ];
// Fix 3: Collect into a new vector
let v = vec! [ 1 , 2 , 3 , 4 , 5 ];
let v2 : Vec < i32 > = v . iter () . map ( |& x | x + 1 ) . collect ();
When you have a mutable reference and you pass it to a function, Rust performs an implicit “reborrow.” The mutable reference is temporarily borrowed, and the callee receives a new mutable Reference with a potentially shorter lifetime:
fn push (item : &mut String ) {
let mut s = String :: from ( "hello" );
push (r); // reborrow: r is temporarily borrowed by push
println! ( "{}" , r); // OK — the reborrow ended, r is valid again
Without reborrows, the above code would fail because push(r) would move rMaking it unusable Afterward. The reborrow mechanism makes &mut references behave more ergonomically.
You can create an explicit reborrow with &mut *r:
fn push (item : &mut String ) {
let mut s = String :: from ( "hello" );
let r : &mut String = &mut s;
let r2 : &mut String = &mut * r; // explicit reborrow
// r2 is no longer usable here (it was moved into push)
// But r is still valid because the reborrow from r has ended
The original borrow checker tied borrows to lexical scopes. A borrow lived until the end of the Scope in which it was created, regardless of actual usage:
// In lexical lifetimes: r's borrow extends to the end of main()
// let y = &mut x; // ERROR with lexical lifetimes (even though r is unused)
NLL analyzes the control flow graph to determine when a reference is last used. The borrow ends at The last usage point, not at the end of the lexical scope:
// NLL: r's borrow ends here (last use)
let y = &mut x; // OK with NLL
Polonius is the next-generation borrow checker, named after the character from Hamlet (“I have of Late, but wherefore I know not, lost all my mirth”). It uses a dataflow analysis approach that is Both more precise and easier to reason about than NLL. As of Rust 1.85, Polonius is available as an Experimental feature (-Zpolonius) and is expected to become the default in a future edition.
Polonius enables patterns that NLL rejects, such as:
// This pattern is rejected by NLL but accepted by Polonius:
fn filter_map (vec : &mut Vec < i32 >) {
let last = vec . iter () . last ();
if let Some ( & item) = last {
vec . retain ( |& x | x != item);
Fighting the borrow checker with clone(). While clone() works, it often indicates a design problem. Before cloning, consider: can you restructure ownership? Can you use indices instead of references? Can you borrow for a shorter lifetime? Clone is correct when you genuinely need a second independent copy of the data.
Self-referential structs. Rust cannot express structs that hold references to their own fields because the lifetime of the reference and the lifetime of the struct are the same. Use indices instead, or crates like pin-project and owning-ref for self-referential patterns.
RefCell panics in production. RefCell::borrow_mut() panics at runtime if there is an outstanding immutable borrow. In a long-running service, this can bring down the process. Use try_borrow_mut() and handle the error, or restructure your code to avoid the need for RefCell.
Rc reference cycles. If two Rc values reference each other, neither will ever be dropped. Use Weak<T> to break cycles. Profile your application with tools like valgrind or heaptrack to detect reference cycle leaks.
Borrowing across await points. Holding a reference across an .await point is an error because the future may be dropped or moved between yields, invalidating the reference. Restructure the code to drop the borrow before awaiting.
Ignoring lifetime variance. Lifetimes are covariant in output position and invariant in mutable reference position. Misunderstanding this leads to subtle soundness bugs when writing generic code over lifetimes. The compiler errors in these cases are often confusing.
Using unsafe to bypass the borrow checker. unsafe lets you create raw pointers and convert them to references, but you are now responsible for maintaining all borrow checker invariants manually. A single violation (e.g., creating two mutable references to the same data) is undefined behavior, even if it appears to work.
Over-borrowing in closures. Closures capture variables by the least-permissive mode needed. &x for immutable access, &mut x for mutable access, x for ownership. A closure that modifies a captured variable will capture it by &mutPreventing any other borrow of the same variable while the closure exists. Use move closures to transfer ownership into the closure and avoid borrow conflicts.
Confusing 'a lifetime names with actual lifetimes. The name 'a is just a placeholder. The compiler substitutes the actual lifetime at each call site. Two functions using 'a in their signatures do not necessarily share the same lifetime — the compiler resolves each independently.
Not understanding NLL. If the borrow checker rejects your code, check whether the borrow is actually needed past the point where the compiler thinks it ends. Often, adding an explicit block scope {} or dropping a reference early fixes the issue without restructuring.
graph TD
A[Need to access data?] --> B{Read only?}
B -->|Yes| C{Single reference?}
B -->|No| D{Single mutable reference?}
C -->|Yes| E[Use &T — immutable borrow]
C -->|No| F{All borrows immutable?}
F -->|Yes| E
F -->|No| G[ERROR: cannot mix &T and &mut T]
D -->|Yes| H[Use &mut T — mutable borrow]
D -->|No| I[ERROR: cannot have multiple &mut T]
E --> J{Does the borrow outlive the value?}
H --> J
J -->|Yes| K[ERROR: dangling reference]
J -->|No| L[OK — borrow is valid] Lifetimes interact with generics in ways that can be subtle. When a generic type parameter is Bounded by a lifetime, it constrains which concrete types can be used:
// T must outlive 'a — T must be a type that can be borrowed for 'a
fn process <' a , T : "a>(value: &''a T) -> &" a T {
// Without the T: "a bound, this would not compile
// because the compiler cannot prove T is valid for ''a
This bound is automatically added (lifetime elision), but you may need to write it Explicitly when working with trait objects or complex generic constraints:
fn process ( & self ) -> Self :: Output ;
// This requires T: "a because the trait object might reference data with lifetime 'a
fn run <' a , T >(processor : & ' a dyn Processor < Output = T >) -> T
Higher-rank trait bounds express constraints on lifetimes that are universally quantified (forall). The most common use is with Fn traits:
// This function accepts a closure that works with ANY lifetime ''a
F : for < "a> Fn(&'a str) -> &'a str,
let s = String::from(" hello ");
The for<'a> syntax means “for all lifetimes ‘a.” The closure must be valid regardless of what Lifetime 'a the caller chooses. This is a more restrictive bound than specifying a single lifetime Because the closure cannot capture references with a specific lifetime.
HRTBs are also used in the standard library for Iterator::find:
impl < I : Iterator > Iterator for I {
fn find < P >( &mut self , predicate : P ) -> Option < Self :: Item >
P : FnMut ( & Self :: Item ) -> bool ,
// The predicate must accept references with the lifetime of self
One of the most common borrow checker challenges is creating structs that reference their own Fields. This is fundamentally impossible in safe Rust because the struct and its field share the Same lifetime, but the borrow checker treats them as independent:
// This does NOT compile:
pointer : & str , // what lifetime? The struct's own lifetime?
data : String :: from ( "hello" ),
pointer : & String :: from ( "hello" ), // dangling reference
Index-based approach (most common):
edges : Vec < usize >, // indices into the graph's nodes Vec
Arena allocation (via crates like typed-arena or bumpalo):
let a = arena . alloc ( "hello" );
let b = arena . alloc ( "world" );
// a and b have the same lifetime — references between them are valid
Pin-based approach for async state machines:
// The pointer is valid as long as the struct is pinned
Understanding variance is critical when writing generic code over lifetimes. Variance determines Whether a longer lifetime can be substituted for a shorter one.
&'a T is covariant in 'a. If 'long: "short (long outlives short), then &''long T can be used Where &"short T is expected. This is safe because a longer-lived reference is a subtype of a Shorter-lived one when you only read through it.
fn takes_short <' a >(r : & ' a str ) {}
let long : & ' static str = "hello" ;
takes_short (long); // OK — 'static can be shortened to 'a
&'a mut T is invariant in 'a. You cannot substitute a longer-lived &'long mut T where a &'short mut T is expected. This prevents soundness issues where a mutable reference to a Shorter-lived value could be used to write a longer-lived reference, extending its lifetime beyond Its valid scope.
fn takes_short_mut <' a >(r : & ' a mut i32 ) {}
let r : & ' static mut i32 = unsafe { &mut * Box :: into_raw ( Box :: new (x)) };
// takes_short_mut(r); // ERROR — &'static mut i32 cannot be shortened to &'a mut i32
fn(&'a T) -> &'a T is contravariant in the argument lifetime and covariant in the return lifetime. This means you can pass a function expecting a shorter lifetime where a function expecting a longer Lifetime is needed (because it accepts a superset of lifetimes).
The borrow checker also enforces drop order correctness. When a struct contains a reference, the Compiler must ensure that the struct does not outlive the referenced data, even during destruction:
// The compiler ensures that Context is dropped before the data it references
// This is automatic — you do not need to write anything special
The drop checker can be overly conservative. If your struct contains a raw pointer that does not Actually reference the lifetime parameter, you can use the #[may_dangle] attribute (unsafe) to Relax the drop check. This is advanced and should be used only when you can prove safety manually.
This topic covers the core concepts of ownership and borrowing, including underlying theory, practical implementation, and key applications.
Key concepts include:
ownership, borrowing, and lifetimes structs, enums, and pattern matching traits and generics error handling (Result, Option) concurrency with threads and async Understanding these concepts thoroughly is essential for both examinations and practical programming, and requires both theoretical knowledge and hands-on practice.
Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.
## Intuition
Rust’s ownership system enforces three rules at compile time: each value has exactly one owner, ownership transfers on assignment (move), and you can have either many immutable references or one mutable reference. The borrow checker operates on MIR with zero runtime cost. Move semantics transfer the pointer without copying heap data, while the Copy trait marks types that duplicate on assignment. This eliminates garbage collection overhead while preventing dangling pointers and data races.
[[rust/02-ownership-borrowing/lifetimes]] - Lifetime annotations and scope tracking [[rust/02-ownership-borrowing/interior-mutability]] - RefCell and interior mutability patterns [[rust/03-structs-enums/structs-and-enums]] - Structs as owner containers [[rust/04-error-handling/error-handling]] - Result and Option for error propagation