Back to blog

Sunday, June 7, 2026

Rust Ownership

cover

Introduction

Rust is a systems programming language that promises both performance and safety, especially around memory management. Its secret lies in a set of compile‑time rules called ownership. Unlike languages that rely on garbage collectors or manual memory management, ownership ensures memory is cleaned up automatically and predictably without runtime overhead. In this blog, we’ll explore what ownership is, how it works, and how you can use it to write safe, efficient Rust code.

Understanding Ownership

Ownership is Rust’s central feature for managing memory. Every value in Rust has a single variable that is its owner. When the owner goes out of scope, the value is dropped (its memory is freed). This simple rule, combined with the borrow checker, eliminates entire classes of bugs like dangling pointers, double frees, and data races at compile time.

Why Ownership?

  • No garbage collector – predictable performance, suitable for embedded and real‑time systems.
  • Memory safety – no null pointer exceptions, use‑after‑free, or buffer overruns.
  • Thread safety – ownership rules naturally prevent data races.

Ownership Rules

  1. Each value in Rust has a single owner – the variable that binds it.
  2. There can only be one owner at a time – when a value is moved, the previous owner can no longer access it.
  3. When the owner goes out of scope, the value is dropped – the memory is returned to the system.

Let’s see these rules in action.

Moving a Value

let s1 = String::from("hello");
let s2 = s1;  // s1 is moved to s2

// println!("{}", s1); // error! s1 is no longer valid
println!("{}", s2);     // works fine

Here, s1 is moved to s2. Rust invalidates s1 to prevent a double‑free. Only s2 now owns the heap‑allocated string.

Clone instead of Move

If you need a deep copy, use the clone() method.

let s1 = String::from("hello");
let s2 = s1.clone();

println!("s1 = {}, s2 = {}", s1, s2); // both are valid

Stack vs. Heap

Understanding where data lives is crucial for ownership.

  • Stack – stores fixed‑size data (like integers, booleans). Values are copied, not moved. These types implement the Copy trait.
  • Heap – stores variable‑size data (like String, Vec<T>). Ownership transfers when assigning or passing to a function (move semantics).
let x = 5;
let y = x;    // copy, both x and y are valid
println!("x = {}, y = {}", x, y);

let s1 = String::from("hello");
let s2 = s1;  // move, s1 is no longer usable

Ownership and Functions

Passing a value to a function will either move or copy it, following the same rules. Returning a value can transfer ownership back.

fn main() {
    let s = String::from("hello");
    takes_ownership(s);          // s moved into function
    // println!("{}", s);       // error

    let x = 5;
    makes_copy(x);               // x is i32, copied
    println!("{}", x);           // still valid
}

fn takes_ownership(some_string: String) {
    println!("{}", some_string);
} // some_string goes out of scope, memory freed

fn makes_copy(some_integer: i32) {
    println!("{}", some_integer);
}

Returning ownership:

fn main() {
    let s1 = gives_ownership();
    let s2 = String::from("hello");
    let s3 = takes_and_gives_back(s2);
    // s1 and s3 are valid, s2 was moved
}

fn gives_ownership() -> String {
    let some_string = String::from("yours");
    some_string                     // moved to caller
}

fn takes_and_gives_back(a_string: String) -> String {
    a_string                        // returned, ownership moves
}

References and Borrowing

Instead of transferring ownership, you can let a function borrow a value by using references.

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);  // &s1 creates a reference
    println!("The length of '{}' is {}.", s1, len);
} // s1 still owns the string

fn calculate_length(s: &String) -> usize {
    s.len()
} // s goes out of scope but does not free the string

The & symbol creates a reference that allows you to use a value without taking ownership. References are immutable by default.

Mutable References

You can also borrow mutably, but with strict rules: only one mutable reference to a particular piece of data in a particular scope.

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

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

The rule prevents data races at compile time.

Dangling References

Rust will not allow you to create a dangling reference – a reference that points to invalid memory.

// This will NOT compile
fn dangle() -> &String {
    let s = String::from("hello");
    &s // error: s goes out of scope, so the reference is invalid
}

Slices

Slices let you reference a contiguous sequence of elements in a collection without taking ownership.

String Slices

let s = String::from("hello world");
let hello = &s[0..5];  // "hello"
let world = &s[6..11]; // "world"

A slice is a reference to a portion of the string, defined by &str. It avoids copying and is tied to the original string’s lifetime.

fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }
    &s[..]
}

Other Slices

Arrays and vectors can also be sliced.

let a = [1, 2, 3, 4, 5];
let slice = &a[1..3]; // &[i32] type
assert_eq!(slice, &[2, 3]);

Lifetimes (Briefly)

Lifetimes ensure that references are always valid. Most of the time they are elided (inferred), but sometimes you need to annotate them to tell the compiler how references relate.

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

Here, 'a indicates that the returned reference lives as long as the shorter of x and y. Lifetime annotations become essential when a function returns a reference that depends on multiple input references.

Common Patterns and Best Practices

  • Use immutable references by default; only use mutable borrows when necessary.
  • Keep lifetimes short to make code easier to reason about.
  • Prefer passing slices (&str, &[T]) over &String or &Vec<T> for more flexible APIs.
  • When you need to transfer ownership, use clone() deliberately – avoid unnecessary copies.

Putting It All Together: A Safe Program

Here’s a small example that reads a string, finds the first word, and prints it, all while respecting ownership and borrowing.

fn main() {
    let s = String::from("Rust is awesome");
    let word = first_word(&s);   // immutable borrow
    println!("First word: {}", word);
    // s is still usable here
}

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[..i];
        }
    }
    &s[..]
}

Because first_word takes a &str (a slice), it works with both String and string literals.

Conclusion

Ownership is the backbone of Rust’s memory safety guarantees. By enforcing strict rules about moves, borrowing, and lifetimes at compile time, Rust eliminates entire classes of bugs while retaining low‑level control. Once you internalize these concepts, you’ll find that the compiler becomes a helpful guide rather than an obstacle, steering you toward reliable and efficient code. Practice with small programs, experiment with moves and borrows, and soon ownership will become second nature.

Additional Resources

By mastering Rust’s ownership model, you’ll be equipped to write high‑performance applications with confidence, knowing that the compiler has your back.