Skip to content

Ownership and Borrowing

## The Ownership Rules

Rust’s memory management rests on three rules enforced at compile time:

  1. Each value in Rust has a single owner.
  2. When the owner goes out of scope, the value is dropped (memory is freed).
  3. 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.

fn main() {
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:

CategoryExamplesBehavior
Copy typesi32``f64``bool``char``(i32, i32)``&TAssignment copies the value
Move typesString``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.

#[derive(Copy, Clone)]
struct Point {
x: f64,
y: f64,
}
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:

struct Person {
name: String,
age: u32,
}
let person = Person {
name: String::from("Alice"),
age: 30,
};
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) {
println!("{}", s);
} // s is dropped here
fn main() {
let s = String::from("hello");
takes_ownership(s);
// println!("{}", s); // ERROR: s was moved
}

To avoid the move, pass a reference:

fn borrows(s: &String) {
println!("{}", s);
}
fn main() {
let s = String::from("hello");
borrows(&s);
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");
let r1 = &s;
let r2 = &s;
let r3 = &s;
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 r1 = &mut s;
// let r2 = &mut s; // ERROR: cannot borrow as mutable more than once
r1.push_str(", world");
println!("{}", r1);

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
r2.push_str(", world");
println!("{}", r2);

The borrow checker guarantees that references always point to valid data. This is one of Rust’s most Important safety guarantees:

fn dangle() -> &String {
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) │ ✓
│ &T &mut T (mixed) │ ✗
│ &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 {
if x.len() > y.len() {
x
} else {
y
}
}

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) -> &strfn 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.

impl Foo {
fn method(&self, x: &str) -> &strfn 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) {
println!("{} {}", x, y);
}

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:

struct Excerpt<'a> {
part: &'a str,
}
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence;
{
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 {
x
}

'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
## 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