4 min read
On this page

References & Borrowing

Moving ownership every time you want to use a value would be exhausting. You would have to pass values into functions and get them back out constantly. Borrowing solves this: you lend a value to someone without giving up ownership. References are how Rust implements borrowing, and the rules governing them are what prevent data races at compile time.

Immutable References: &T

An immutable reference lets you read a value without owning it. You can have as many immutable references as you want simultaneously.

fn calculate_length(s: &String) -> usize {
    s.len()
    // s goes out of scope, but since it's a reference,
    // the original String is not dropped
}

fn main() {
    let s = String::from("hello");
    let len = calculate_length(&s); // borrow s
    println!("'{}' has length {}", s, len); // s is still valid
}
'hello' has length 5

The & creates a reference. The function receives a reference to the String without taking ownership. After the function returns, s is still valid in main.

Multiple immutable borrows are fine:

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

    let r1 = &data;
    let r2 = &data;
    let r3 = &data;

    println!("r1: {:?}, r2: {:?}, r3: {:?}", r1, r2, r3);
}
r1: [1, 2, 3, 4, 5], r2: [1, 2, 3, 4, 5], r3: [1, 2, 3, 4, 5]

This is safe because nobody is modifying the data. Multiple readers, no writers — no problem.

Mutable References: &mut T

A mutable reference lets you modify a borrowed value. The critical rule: you can have exactly one mutable reference to a value at a time, and no immutable references can coexist with it.

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

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

Note that both the variable (let mut s) and the reference (&mut s) must be marked mutable. Rust makes mutation visible at every point in the chain.

The Exclusivity Rule

You cannot have a mutable reference while immutable references exist. This is not arbitrary — it prevents data races.

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!("{} and {}", r1, r2);
    // r1 and r2 are no longer used after this point

    let r3 = &mut s;  // now this is fine — no immutable borrows are active
    r3.push_str("!");
    println!("{}", r3);
}
hello and hello
hello!

The borrow checker uses Non-Lexical Lifetimes (NLL): a reference's lifetime ends at its last use, not at the end of the scope. This is why r3 works after r1 and r2 are done being used.

Why This Prevents Data Races

A data race requires three conditions:

  1. Two or more pointers access the same data simultaneously
  2. At least one pointer is writing
  3. There is no synchronization

Rust's borrowing rules make condition 1+2 impossible at compile time. Either you have multiple readers (all immutable references) or one writer (one mutable reference). Never both.

use std::thread;

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

    // This would not compile — Rust prevents the data race:
    // let r = &data;
    // thread::spawn(move || {
    //     data.push(4); // would need &mut data
    // });
    // println!("{:?}", r); // r still borrowed immutably

    // The safe version uses proper synchronization:
    let handle = thread::spawn(move || {
        data.push(4); // data moved into the thread
        data
    });

    let data = handle.join().unwrap();
    println!("{:?}", data);
}
[1, 2, 3, 4]

References in Structs & Functions

Passing references is the bread and butter of Rust APIs. Prefer borrowing over owning when a function only needs to read data:

struct Config {
    database_url: String,
    max_retries: u32,
}

// Takes a reference — does not consume the Config
fn connect(config: &Config) -> Result<(), String> {
    println!("Connecting to {} (max retries: {})",
        config.database_url, config.max_retries);
    Ok(())
}

// Takes a mutable reference — modifies the Config in place
fn set_retries(config: &mut Config, retries: u32) {
    config.max_retries = retries;
}

fn main() {
    let mut config = Config {
        database_url: String::from("postgres://localhost/mydb"),
        max_retries: 3,
    };

    connect(&config).unwrap();
    set_retries(&mut config, 5);
    connect(&config).unwrap();
}
Connecting to postgres://localhost/mydb (max retries: 3)
Connecting to postgres://localhost/mydb (max retries: 5)

Slices: References to Parts of Collections

Slices are references to a contiguous sequence of elements, not the whole collection:

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
}

fn sum_first_three(numbers: &[i32]) -> i32 {
    numbers.iter().take(3).sum()
}

fn main() {
    let sentence = String::from("hello beautiful world");
    let word = first_word(&sentence);
    println!("First word: {}", word);

    let nums = vec![10, 20, 30, 40, 50];
    let total = sum_first_three(&nums);
    println!("Sum of first three: {}", total);
}
First word: hello
Sum of first three: 60

&str is a string slice — a reference to a portion of string data. &[i32] is a slice of integers. These are the idiomatic parameter types when you just need to read data.

Reborrowing

You can create an immutable reference from a mutable one. This is called reborrowing and is implicit:

fn print_value(s: &String) {
    println!("{}", s);
}

fn main() {
    let mut s = String::from("hello");
    let r = &mut s;
    print_value(r);    // implicitly reborrows &*r (immutable from mutable)
    r.push_str("!");   // r is still usable
    println!("{}", r);
}
hello
hello!

The compiler automatically creates a temporary immutable reference from the mutable one. This is safe because the mutable reference is "paused" during the function call.

Interior Mutability

Sometimes you need to mutate data behind an immutable reference. Rust provides types for this that move the borrow check to runtime:

use std::cell::RefCell;

struct Logger {
    messages: RefCell<Vec<String>>,
}

impl Logger {
    fn new() -> Self {
        Logger {
            messages: RefCell::new(Vec::new()),
        }
    }

    fn log(&self, msg: &str) {
        // &self is immutable, but RefCell allows mutation
        self.messages.borrow_mut().push(msg.to_string());
    }

    fn dump(&self) {
        for msg in self.messages.borrow().iter() {
            println!("[LOG] {}", msg);
        }
    }
}

fn main() {
    let logger = Logger::new();
    logger.log("Application started");
    logger.log("Processing request");
    logger.dump();
}
[LOG] Application started
[LOG] Processing request

RefCell<T> enforces borrowing rules at runtime instead of compile time. It will panic if you violate them. Use it sparingly — it exists for cases where the compiler cannot prove safety but you can.

Common Pitfalls

  • Holding references longer than needed. If a reference keeps a large data structure alive, move the reference use earlier or restructure the code.
  • Trying to return a reference to local data. A function cannot return &String if the String was created inside the function — the data will be freed when the function returns. Return the owned String instead.
  • Confusing &String and &str. Prefer &str in function parameters. It accepts both &String and string literals. &String is almost never what you want in a function signature.
  • Using RefCell when restructuring would be cleaner. Interior mutability is a tool of last resort. If you can redesign your data flow to avoid it, do so.
  • Forgetting that &mut requires the binding to be mut too. You need let mut x = ... before you can create &mut x. The variable itself must be declared as mutable.

Key Takeaways

  • Immutable references (&T) allow multiple simultaneous readers.
  • Mutable references (&mut T) allow exactly one writer, with no readers.
  • These rules prevent data races at compile time, not runtime.
  • Prefer &str over &String and &[T] over &Vec<T> in function parameters.
  • The borrow checker uses Non-Lexical Lifetimes: references end at their last use, not the end of scope.
  • Interior mutability (RefCell, Mutex) moves borrow checking to runtime when compile-time checking is insufficient.