Skip to editor content
learningrust.orglesson 8 of 26

Borrowing in Depth

Building on what we learned about ownership, this lesson takes a deeper look at borrowing - one of Rust's most powerful features for writing safe, efficient code without copying data.

Why Borrowing Matters

Without borrowing, you'd need to pass ownership around constantly, which can be inconvenient:

fn main() {
    let s1 = String::from("hello");
    let (s2, len) = calculate_length(s1);
    // s1 is moved, can't use it anymore!
    println!("The length of '{}' is {}.", s2, len);
}

// Awkward: returns the string back along with the length
fn calculate_length(s: String) -> (String, usize) {
    let length = s.len();
    (s, length)
}

With borrowing, this becomes much cleaner:

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

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

Immutable References

By default, references are immutable - you can read but not modify:

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

    // Create immutable reference
    let r1 = &s;
    let r2 = &s;  // Multiple immutable refs are OK

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

    println!("Original: {}", s);  // s is still valid
}

Mutable References

You met &mut back in Ownership & Borrowing: to modify borrowed data, the reference itself must be mutable. Here we pick that up and look at what the compiler enforces around it.

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

    change(&mut s);

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

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

Note that both the binding (let mut s) and the reference (&mut s) must be marked mutable — a &mut to an immutable binding won't compile. That single restriction is what powers the rules below.

The Borrowing Rules

Rust enforces these rules at compile time:

Rule 1: One Mutable OR Many Immutable

You can have either:

  • One mutable reference, OR
  • Any number of immutable references

But never both at the same time:

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

    let r1 = &s;     // OK - first immutable ref
    let r2 = &s;     // OK - second immutable ref
    println!("{} and {}", r1, r2);
    // r1 and r2 are no longer used

    let r3 = &mut s; // OK - mutable ref (no immutable refs active)
    println!("{}", r3);
}

This prevents data races:

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

    let r1 = &s;
    // let r2 = &mut s; // ERROR! Can't have mutable while immutable exists

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

Rule 2: References Must Be Valid

References must always point to valid data (no dangling references):

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

// Instead, return the owned value:
fn no_dangle() -> String {
    let s = String::from("hello");
    s  // Ownership is moved out
}

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

Non-Lexical Lifetimes (NLL)

Modern Rust uses NLL - references are considered "active" only until their last use, not until the end of scope:

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

    let r1 = &s;
    let r2 = &s;
    println!("{} and {}", r1, r2);
    // r1 and r2's last use is here ^^^

    // This works because r1 and r2 are no longer "live"
    let r3 = &mut s;
    println!("{}", r3);
}

Reborrowing

You can reborrow from a mutable reference:

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

    // Reborrow: create immutable ref from mutable ref
    let r2 = &*r1;  // or just: let r2 = &r1;
    println!("{}", r2);

    // r1 is still valid after r2 is done
    r1.push_str(" world");
    println!("{}", r1);
}

Borrowing in Structs

Structs can hold references, but need lifetime annotations:

// Simple case: owned data (no lifetimes needed)
struct User {
    name: String,
    age: u32,
}

fn main() {
    let user = User {
        name: String::from("Alice"),
        age: 30,
    };
    println!("User: {}, Age: {}", user.name, user.age);
}

Borrowing Patterns

Pattern 1: Read-Only Access

fn print_info(data: &Vec<i32>) {
    for item in data {
        println!("{}", item);
    }
}

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    print_info(&numbers);
    print_info(&numbers);  // Can borrow again
}

Pattern 2: Modify in Place

fn double_values(data: &mut Vec<i32>) {
    for item in data.iter_mut() {
        *item *= 2;
    }
}

fn main() {
    let mut numbers = vec![1, 2, 3, 4, 5];
    double_values(&mut numbers);
    println!("{:?}", numbers);  // [2, 4, 6, 8, 10]
}

Pattern 3: Split Borrowing

You can borrow different parts of a struct simultaneously:

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let mut point = Point { x: 0, y: 0 };

    let x_ref = &mut point.x;
    let y_ref = &mut point.y;  // OK! Different fields

    *x_ref = 10;
    *y_ref = 20;

    println!("Point: ({}, {})", point.x, point.y);
}

Common Borrowing Errors

Error: Borrowed Value Moved

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

    // let s2 = s;  // ERROR: can't move while borrowed

    println!("{}", r);  // r still in use
}

Error: Mutable Borrow While Immutable Exists

fn main() {
    let mut v = vec![1, 2, 3];
    let first = &v[0];

    // v.push(4);  // ERROR: can't mutate while immutably borrowed

    println!("First: {}", first);
}

Practice Exercise

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

    // Multiple immutable borrows
    let len = get_length(&message);
    let first_char = get_first_char(&message);
    println!("Length: {}, First char: {:?}", len, first_char);

    // Mutable borrow after immutable borrows are done
    append_exclamation(&mut message);
    println!("Final: {}", message);
}

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

fn get_first_char(s: &String) -> Option<char> {
    s.chars().next()
}

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

Key Takeaways

  • Borrowing lets you use data without taking ownership
  • &T creates an immutable reference (read-only)
  • &mut T creates a mutable reference (read-write)
  • You can have many &T OR one &mut T, never both
  • References must always point to valid data
  • NLL makes the borrow checker smarter about when refs are "live"
  • Understanding borrowing is essential for writing idiomatic Rust

Master borrowing and you'll write safe, efficient Rust code!

Next Steps

With borrowing mastered, you're ready for closures — anonymous functions that capture variables from their environment. Because closures borrow (or move) the values they capture, the borrowing rules you just learned are exactly what govern how they behave.

Next lesson

Closures

Rust closures tutorial — learn how closures capture variables, understand Fn, FnMut, and FnOnce traits, and use closures with iterators

25 min