Skip to content
Codeloom
Rust

Rust Ownership and Borrowing Explained

Understand Rust's ownership model — move semantics, borrowing, references, lifetimes, and why the borrow checker exists.

·5 min read · By Codeloom
Beginner 12 min read

What you'll learn

  • What ownership means and why Rust has it
  • Move semantics vs copy semantics
  • Borrowing with immutable and mutable references
  • Basic lifetime annotations

Prerequisites

  • Basic programming concepts (variables, functions)
  • Some familiarity with Rust syntax

Ownership is the feature that makes Rust unique. It eliminates data races, use-after-free, and double-free bugs at compile time — with zero runtime cost.

The three rules of ownership

  1. Each value has exactly one owner (a variable).
  2. When the owner goes out of scope, the value is dropped (freed).
  3. There can only be one owner at a time.
fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // s1 is MOVED to s2

    // println!("{}", s1); // Error: s1 no longer owns the value
    println!("{}", s2);    // Works
}

Move vs Copy

Types that live on the stack and have a known size implement Copy. They are copied instead of moved.

let x = 5;
let y = x; // x is COPIED, both are valid
println!("{} {}", x, y); // Works — integers are Copy

Heap-allocated types like String, Vec, and Box are moved.

let v1 = vec![1, 2, 3];
let v2 = v1; // v1 is moved
// println!("{:?}", v1); // Error

When does Copy apply?

Copy typesMove types
i32, f64, bool, charString, Vec<T>, Box<T>
Tuples of Copy typesStructs (unless you derive Copy)
Fixed-size arrays of Copy typesAnything with heap allocation

Borrowing with references

Instead of transferring ownership, you can borrow a value with a reference.

fn calculate_length(s: &String) -> usize {
    s.len()
}

fn main() {
    let s = String::from("hello");
    let len = calculate_length(&s); // borrow s
    println!("{} has length {}", s, len); // s is still valid
}

&s creates an immutable reference. The function borrows the value but does not own it.

Mutable references

fn add_exclamation(s: &mut String) {
    s.push('!');
}

fn main() {
    let mut s = String::from("hello");
    add_exclamation(&mut s);
    println!("{}", s); // hello!
}

The borrowing rules

  1. You can have any number of immutable references (&T) at the same time.
  2. You can have exactly one mutable reference (&mut T) at a time.
  3. You cannot have both at the same time.
let mut s = String::from("hello");

let r1 = &s;     // OK
let r2 = &s;     // OK — multiple immutable refs
// let r3 = &mut s; // Error — can't borrow mutably while immutable refs exist

println!("{} {}", r1, r2);
// r1 and r2 are no longer used after this point

let r3 = &mut s; // OK — previous refs are no longer in scope
r3.push('!');

Dangling references

Rust prevents dangling references at compile time.

// fn dangle() -> &String {
//     let s = String::from("hello");
//     &s // Error: s is dropped at end of function, reference would dangle
// }

fn no_dangle() -> String {
    let s = String::from("hello");
    s // ownership is moved out — no dangling reference
}

Lifetimes

Lifetimes tell the compiler how long references are valid. Most of the time, Rust infers them. You only need explicit annotations when the compiler cannot figure it out.

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

The 'a annotation says: “the returned reference lives at least as long as the shorter of the two input references.”

fn main() {
    let s1 = String::from("long string");
    let result;
    {
        let s2 = String::from("hi");
        result = longest(&s1, &s2);
        println!("{}", result); // OK — s2 is still alive
    }
    // println!("{}", result); // Error — s2 is dropped, result might reference it
}

Ownership in structs

Structs can own data or borrow it (with lifetimes).

// Owns its data
struct User {
    name: String,
    age: u32,
}

// Borrows data — needs a lifetime
struct UserRef<'a> {
    name: &'a str,
    age: u32,
}

Prefer owned data in structs. Use references only when you need to avoid allocation and the lifetime is clear.

Clone

When you need a deep copy, use .clone().

let s1 = String::from("hello");
let s2 = s1.clone(); // explicit deep copy
println!("{} {}", s1, s2); // both valid

clone() is explicit because it can be expensive. Rust makes you opt in.

Summary

Ownership ensures memory safety without a garbage collector. Values have one owner, references borrow without taking ownership, and the borrow checker enforces that references never outlive their data. Fight the borrow checker at first — then realize it is catching bugs you would have shipped in any other language.