LearningRust.org
LessonsPlaygroundAbout
Sign In
Lessons/intermediate/Ownership & Borrowing
PreviousPracticeNext

TL;DR

Rust ownership explained — understand move semantics, borrowing rules, and how Rust guarantees memory safety without a garbage collector

Key concepts

  • Rust ownership explained
  • Rust move semantics
  • Rust memory safety
  • Rust borrow checker
Loading...

Next lesson

Option and Result

Master Rust Option and Result types for handling absence and errors with combinators, the ? operator, and error propagation

25 min

Related lessons

  • Error HandlingLearn Rust error handling with the Result type, panic!, recoverable vs unrecoverable errors, and the ? operator for propagation
  • Structs & EnumsLearn how to define Rust structs and enums to create custom data types, implement methods, and model your domain effectively
  • Borrowing in DepthDeep dive into Rust borrowing and references — learn shared and mutable references, the borrow checker, and safe data access

Also learn

ZigLearn Zig ProgrammingTypeScriptMaster TypeScript

Also learn

ZigLearn Zig ProgrammingTypeScriptMaster TypeScript

A14A

Building digital products that matter.

© 2026 A14A. All rights reserved.
KVK: 87105004PrivacyTerms

Ownership & Borrowing

Ownership is Rust's most unique feature and has deep implications for the rest of the language. It enables Rust to make memory safety guarantees without needing a garbage collector.

What is Ownership?

Ownership is a set of rules that govern how a Rust program manages memory. Unlike languages with garbage collection (like Java or Python) or manual memory management (like C), Rust uses a third approach: memory is managed through a system of ownership with rules that the compiler checks at compile time.

The Three Rules of Ownership

  1. Each value in Rust has a variable that's called its owner
  2. There can only be one owner at a time
  3. When the owner goes out of scope, the value will be dropped
fn main() {
    // s comes into scope
    let s = String::from("hello");

    // s is valid from this point forward
    println!("{}", s);

} // s goes out of scope and is dropped

The Stack and the Heap

To understand ownership, we need to understand how data is stored:

  • Stack: Fast, fixed-size data (integers, booleans, references)
  • Heap: Dynamic-size data (Strings, Vectors, etc.)
fn main() {
    // i32 is stored on the stack - copy is cheap
    let x = 5;
    let y = x; // y is a copy of x

    println!("x = {}, y = {}", x, y); // Both are valid!

    // String is stored on the heap
    let s1 = String::from("hello");
    let s2 = s1; // s1 is MOVED to s2

    // println!("{}", s1); // ERROR: s1 is no longer valid
    println!("{}", s2); // Only s2 is valid
}

Move vs Copy

Types that have a known size at compile time implement the Copy trait:

fn main() {
    // These types implement Copy
    let a: i32 = 5;
    let b = a; // copy
    println!("a = {}, b = {}", a, b); // Both valid

    // Strings do NOT implement Copy
    let s1 = String::from("hello");
    let s2 = s1; // move, not copy
    // s1 is no longer valid here
}

Borrowing with References

Instead of taking ownership, you can borrow a value using references:

fn main() {
    let s1 = String::from("hello");

    // &s1 creates a reference to s1
    let len = calculate_length(&s1);

    // s1 is still valid because we only borrowed it!
    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
} // s goes out of scope, but it doesn't own the String

Mutable References

By default, references are immutable. To modify borrowed data, use mutable references:

fn main() {
    let mut s = String::from("hello");

    change(&mut s);

    println!("{}", s); // Prints "hello, world"
}

fn change(s: &mut String) {
    s.push_str(", world");
}

The Rules of References

  1. You can have either one mutable reference or any number of immutable references
  2. References must always be valid (no dangling references)
fn main() {
    let mut s = String::from("hello");

    // Multiple immutable references are OK
    let r1 = &s;
    let r2 = &s;
    println!("{} and {}", r1, r2);

    // After r1 and r2 are no longer used, we can have a mutable reference
    let r3 = &mut s;
    r3.push_str(" world");
    println!("{}", r3);
}

Preventing Data Races

This restriction prevents data races at compile time:

fn main() {
    let mut s = String::from("hello");

    let r1 = &mut s;
    // let r2 = &mut s; // ERROR: cannot have two mutable references

    println!("{}", r1);
}

The Slice Type

Slices let you reference a contiguous sequence of elements rather than the whole collection:

fn main() {
    let s = String::from("hello world");

    let hello = &s[0..5];   // or &s[..5]
    let world = &s[6..11];  // or &s[6..]

    println!("{} {}", hello, world);
}

Practice Exercise

Try this code in the playground:

fn main() {
    let mut message = String::from("Hello");

    // Borrow immutably to get length
    let length = get_length(&message);
    println!("Length: {}", length);

    // Borrow mutably to modify
    append_world(&mut message);
    println!("Message: {}", message);
}

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

fn append_world(s: &mut String) {
    s.push_str(", World!");
}

Key Takeaways

  • Ownership ensures memory safety without a garbage collector
  • Each value has exactly one owner
  • When the owner goes out of scope, the value is dropped
  • References allow borrowing without taking ownership
  • You can have one mutable reference OR multiple immutable references
  • The borrow checker enforces these rules at compile time

Understanding ownership is fundamental to writing safe, efficient Rust code!

Next Steps

Now that you understand ownership, you're ready to learn about Option and Result — Rust's two essential enums for handling absence and errors. You've already seen unwrap() and Some/None in earlier lessons; now it's time to master them.

Ready to continue? Head to Option and Result!