TechLead
Lesson 4 of 28
5 min read
Rust

Borrowing & References

Learn Rust borrowing rules, immutable and mutable references, the single-writer principle, and Non-Lexical Lifetimes (NLL).

What is Borrowing?

Instead of transferring ownership, you can borrow a value by creating a reference. References allow you to use a value without taking ownership, so the original owner remains valid. This is Rust's answer to the "I want to read data without consuming it" problem.

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

    // &s creates a reference (borrow)
    let len = calculate_length(&s);

    // s is still valid because we only borrowed it!
    println!("'{s}' has length {len}");
}

// &String means "a reference to a String"
fn calculate_length(s: &String) -> usize {
    s.len()
} // s goes out of scope, but since it doesn't own
  // the data, nothing is dropped

Borrowing Rules

At any given time, you can have either:

  • Any number of immutable references (&T) — multiple readers are safe
  • OR exactly one mutable reference (&mut T) — one writer with exclusive access

You cannot have a mutable reference while immutable references exist. This prevents data races at compile time.

Immutable References (&T)

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

    // Multiple immutable references are fine
    let r1 = &s;
    let r2 = &s;
    let r3 = &s;
    println!("{r1}, {r2}, {r3}"); // All valid

    // Cannot modify through an immutable reference
    // r1.push_str(" world"); // ERROR: cannot borrow as mutable
}

fn print_greeting(name: &str) {
    println!("Hello, {name}!");
    // name is read-only here
}

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
}

Mutable References (&mut T)

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

    // One mutable reference at a time
    change(&mut s);
    println!("{s}"); // "hello, world"

    // This won't compile — two mutable refs at once:
    // let r1 = &mut s;
    // let r2 = &mut s;
    // println!("{r1}, {r2}");

    // But sequential mutable borrows are fine:
    let r1 = &mut s;
    r1.push_str("!");
    // r1 is no longer used after this point

    let r2 = &mut s; // OK — r1's borrow has ended
    r2.push_str("!");
    println!("{s}");
}

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

Cannot Mix Mutable and Immutable

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

    let r1 = &s;     // Immutable borrow
    let r2 = &s;     // Another immutable borrow — fine

    // let r3 = &mut s; // ERROR: cannot borrow as mutable
    //                   // because it's also borrowed as immutable

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

    // Now a mutable borrow is OK!
    let r3 = &mut s;
    r3.push_str(" world");
    println!("{r3}");
}

Non-Lexical Lifetimes (NLL)

Since Rust 2018, the compiler uses Non-Lexical Lifetimes. A reference's lifetime ends at its last use, not at the end of the enclosing scope. This makes the borrow checker much more ergonomic.

fn main() {
    let mut data = vec![1, 2, 3];

    // Before NLL, this wouldn't compile because r's scope
    // extended to the end of the block
    let r = &data;
    println!("Immutable: {:?}", r);
    // NLL: r's borrow ends here (last use)

    // Now we can mutably borrow
    data.push(4);
    println!("Mutated: {:?}", data);
}

Dangling Reference Prevention

// Rust prevents dangling references at compile time!

// This won't compile:
// fn dangle() -> &String {
//     let s = String::from("hello");
//     &s // ERROR: s is dropped at end of function,
//        // so the reference would be dangling!
// }

// Fix: return the owned value instead
fn no_dangle() -> String {
    let s = String::from("hello");
    s // Ownership is moved out — no dangling reference
}

fn main() {
    let s = no_dangle();
    println!("{s}");
}

Key Takeaways

  • ✅ References (&T) borrow data without taking ownership
  • ✅ You can have many immutable references OR one mutable reference — never both
  • ✅ Mutable references (&mut T) require the original variable to be declared mut
  • ✅ NLL makes the borrow checker smarter — borrows end at last use, not scope end
  • ✅ The compiler prevents dangling references entirely at compile time

Continue Learning