🚧 We are still building the lighthouse. Hang tight!

projectlighthouse logoprojectlighthouse

Sign in with your Google or GitHub account to get started

Borrowing and References

Understand borrowing rules - multiple immutable references OR one mutable reference. Learn how Rust prevents data races at compile time through references and the borrow checker.

Transferring ownership to every function that needs to read data creates friction. You pass a value to a function, the function takes ownership, then you need the function to return ownership if you want to use the value again. This works but feels cumbersome for simple operations like reading the length of a string or printing a value.

Borrowing solves this. Instead of transferring ownership, you lend a reference to the data. The function can access the data without owning it. When the function finishes, the reference disappears and the original owner retains ownership. The borrow checker enforces rules that prevent data races at compile time—making concurrent programming safe without runtime overhead.

References Don't Own Data

A reference lets you access a value without taking ownership. Creating a reference is called borrowing:

let s = String::from("hello");
let len = calculate_length(&s);
println!("{} has length {}", s, len);

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

The function calculate_length takes a reference &String. The & operator creates a reference to s. Inside the function, s refers to the string but doesn't own it. When the function returns, the reference goes out of scope, but nothing drops because references don't own data.

After calling calculate_length(&s), the variable s remains valid. Ownership never transferred.

References point to data owned by another variable. They have their own memory address containing a pointer to the actual data. Dereferencing accesses the pointed-to value, but in most contexts Rust dereferences automatically:

fn print_length(s: &String) {
    println!("length: {}", s.len()); // Automatic dereference
    println!("length: {}", (*s).len()); // Explicit dereference
}

Both forms work. The automatic dereferencing makes code cleaner without sacrificing clarity.

Immutable References

References created with & are immutable. You can read the data but can't modify it:

fn modify(s: &String) {
    s.push_str(" world"); // Error: cannot borrow as mutable
}

The compiler rejects this. An immutable reference provides read-only access.

You can have multiple immutable references to the same data simultaneously:

let s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2); // Valid

Multiple readers cause no problems. They're all reading—not modifying—so there's no risk of data races.

Mutable References

To modify borrowed data, create a mutable reference with &mut:

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

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

The variable s must be declared mutable with let mut. The function takes a mutable reference &mut String. Inside the function, you can modify the string.

Mutable references have a crucial restriction: you can have only one mutable reference to a particular piece of data in a particular scope:

let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; // Error: cannot borrow as mutable more than once

This restriction prevents data races. A data race occurs when these three conditions are met: two or more pointers access the same data simultaneously, at least one pointer writes to the data, and there's no synchronization mechanism coordinating access. Rust makes data races impossible by enforcing that you can't have multiple mutable references or mix mutable and immutable references.

The Borrowing Rules

The borrow checker enforces two rules that prevent data races:

First, you can have either one mutable reference OR any number of immutable references, but not both simultaneously.

Second, references must always be valid—they can't outlive the data they reference.

These rules apply at compile time. The borrow checker analyzes your code and rejects programs that violate them. At runtime, there's zero cost for these checks—they're purely static analysis.

The first rule prevents simultaneous reading and writing:

let mut s = String::from("hello");
let r1 = &s;     // OK: immutable reference
let r2 = &s;     // OK: another immutable reference
let r3 = &mut s; // Error: cannot borrow as mutable when immutable refs exist

While immutable references exist, the data is locked for reading. A mutable reference would violate the guarantee that the data won't change while readers access it.

Reference lifetimes are scope-based. Once a reference is no longer used, it's considered out of scope:

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

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

let r3 = &mut s; // OK: no immutable references are active
println!("{}", r3);

This is non-lexical lifetimes (NLL)—the compiler tracks when references are actually used, not just when they go out of scope. After the last use of r1 and r2, the mutable reference r3 is valid because no active immutable references exist.

Dangling References

A dangling reference points to memory that's been freed. Languages without garbage collection often produce dangling references when you free memory but retain pointers to it:

fn dangle() -> &String { // Error: missing lifetime specifier
    let s = String::from("hello");
    &s
} // s goes out of scope and drops here

The function creates a string and returns a reference to it. When the function returns, s goes out of scope and the string drops. The reference points to freed memory—a dangling pointer.

The compiler rejects this with a lifetime error. It knows the reference can't be valid because it points to data that no longer exists. The solution is to return the owned value instead of a reference:

fn no_dangle() -> String {
    let s = String::from("hello");
    s
} // Ownership moves to caller

Now ownership transfers to the caller. The string doesn't drop when the function returns.

Slices

A slice is a reference to a contiguous sequence of elements in a collection. Slices don't own data—they borrow it.

String slices reference part of a string:

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

The slice &s[0..5] references the first five bytes of the string. The slice type is &str. The range syntax 0..5 is exclusive of the end—it includes indices 0, 1, 2, 3, 4.

Slice syntax shortcuts:

let s = String::from("hello");
let slice = &s[0..2];  // Indices 0 and 1
let slice = &s[..2];   // Same: start omitted defaults to 0
let slice = &s[3..];   // From index 3 to end
let slice = &s[..];    // Entire string

String literals are slices pointing to binary data embedded in the program:

let s: &str = "Hello, world!";

The type &str is an immutable reference to string data. String literals have 'static lifetime—they exist for the program's entire execution.

Array slices work similarly:

let numbers = [1, 2, 3, 4, 5];
let slice = &numbers[1..3];
println!("{:?}", slice); // Prints [2, 3]

The slice type is &[i32]—a reference to a slice of i32 values.

Slices are useful for functions that need to work with part of a collection without taking ownership:

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

The function accepts a string slice and returns a string slice pointing to the first word. It doesn't allocate or copy data—just returns a reference to part of the input.

Borrowing Prevents Iterator Invalidation

Consider a common bug in languages with manual memory management or weak borrowing rules: modifying a collection while iterating over it. This invalidates iterators, causing undefined behavior.

Rust's borrow checker prevents this:

let mut vec = vec![1, 2, 3];
for i in &vec {
    vec.push(4); // Error: cannot borrow `vec` as mutable while immutable ref exists
}

The loop creates an immutable reference to vec for iteration. Inside the loop, vec.push(4) requires a mutable reference. The borrow checker rejects this—you can't have a mutable reference while an immutable reference exists.

This prevents a whole class of bugs where modifying a collection invalidates iterators, causing crashes or incorrect results.

Borrowing in Structs

Structs can contain references, but this requires lifetime annotations (covered in a later chapter):

struct User {
    name: String,    // Owned data
    email: String,   // Owned data
}

The struct owns its data. When the struct drops, the strings drop.

To store references in structs, you must specify lifetimes to ensure the references remain valid as long as the struct exists. This prevents the struct from holding references to data that's been freed.

Why Borrowing Prevents Data Races

A data race happens when:

  • Two or more threads access the same memory
  • At least one access is a write
  • Accesses aren't synchronized

Rust's borrowing rules prevent data races at compile time. The rules ensure you can't have multiple mutable references or mix mutable and immutable references. This makes certain data race patterns impossible to express.

If you need shared mutable state across threads, Rust provides types that enforce synchronization through the type system. Mutex<T> ensures only one thread can access data at a time. Arc<T> provides thread-safe reference counting. These types work with the borrow checker to guarantee safety.

Borrowing as a Library Book

Borrowing in Rust resembles borrowing a library book. When you borrow a book, you can read it. Multiple people can borrow the same book for reading (immutable references). If you want to annotate the book, only one person can borrow it at a time (mutable reference). When you return the book, the library retains ownership. You never owned the book—you just had temporary access.

If the library destroys the book before you return it, your library card would point to a destroyed book. Rust prevents this by ensuring references can't outlive the data they reference.

This analogy captures the core concept: borrowing provides temporary access without transferring ownership. The borrow checker ensures access patterns are safe and references remain valid.

When to Borrow vs Take Ownership

Functions that just need to read data should take references. This lets callers retain ownership and reuse values after the function returns.

Functions that need to modify data in place should take mutable references. The caller keeps ownership but allows temporary mutation.

Functions that consume or transform data should take ownership. This signals that the value moves into the function and the caller can't use it afterward.

These patterns communicate intent through type signatures:

fn read_config(path: &Path) -> Result<String, Error>
fn update_cache(cache: &mut HashMap<String, String>)
fn send_data(buffer: Vec<u8>) -> Result<(), Error>

The first reads a path without taking ownership. The second modifies a cache in place. The third consumes a buffer, transferring ownership.

Borrowing Makes Rust Safe and Fast

Borrowing eliminates entire categories of bugs while maintaining performance. The borrow checker runs at compile time, so there's no runtime cost. The generated machine code contains no reference checking—just straightforward memory access.

Languages with garbage collection achieve safety through runtime memory management. Languages with manual memory management achieve performance but sacrifice safety. Rust achieves both through compile-time enforcement of borrowing rules.

The learning curve is steep. The borrow checker will reject code that compiles in other languages. You'll spend time restructuring code to satisfy borrowing rules. The payoff is programs that compile with strong guarantees about memory safety and data race freedom. When your concurrent Rust program compiles, it won't have data races. This guarantee eliminates debugging time that other languages spend hunting race conditions that manifest only under load.

Notes & Highlights

© 2025 projectlighthouse. All rights reserved.