What is the difference between immutable and mutable borrows

Immutable borrows allow read-only access, while mutable borrows allow modification but require exclusive access to the data.

You're editing a config file while someone reads it

You're writing a game loop. The renderer needs to read the player's position every frame. The input handler needs to update that position based on key presses. If the renderer reads the position while the input handler is halfway through writing a new value, the renderer might draw the player at coordinates that make no sense. Half the X, all the Y. The game breaks.

Rust stops this before it happens by splitting access into two modes. You can have many readers, or exactly one writer. Never both. This rule eliminates data races and use-after-free bugs without a garbage collector. The compiler checks every borrow at compile time. If the rules are violated, your code does not run.

The whiteboard rule

Think of a whiteboard in a shared office. Anyone can walk in and read what's written. That's an immutable borrow. You can have fifty people reading the whiteboard at once. Nothing breaks. The text stays consistent.

If someone grabs the marker to write, they need exclusive access. If someone else is writing, the board gets messy. If someone is reading while you erase, they see half-erased text. Rust enforces this rule strictly. You can have many readers, or exactly one writer. The compiler tracks every reference and rejects code that violates the rule.

The compiler enforces this rule at compile time. No data races, no garbage collector, no undefined behavior.

Minimal example

fn main() {
    let mut data = 42;

    // Immutable borrow: safe to share, read-only.
    // Multiple of these can exist simultaneously.
    let r1 = &data;
    let r2 = &data;
    println!("Readers see: {}, {}", r1, r2);

    // Mutable borrow: exclusive access, can modify.
    // No other borrows can exist while this is alive.
    let m = &mut data;
    *m = 100;
    println!("Writer changed value to: {}", m);
}

When you write &data, you create a reference of type &T. The compiler tracks this reference. It allows other &T references to coexist. When you write &mut data, you create &mut T. The compiler checks that no &T or &mut T references are currently active. If they are, compilation fails. The mutable borrow guarantees you are the only one touching the data.

Realistic usage in structs

In real code, borrows appear in method signatures. The signature tells callers what access you need.

struct Inventory {
    items: Vec<String>,
}

impl Inventory {
    /// Returns a snapshot of item count without modifying state.
    fn count(&self) -> usize {
        self.items.len()
    }

    /// Adds an item, requiring exclusive access to the vector.
    fn add(&mut self, item: String) {
        self.items.push(item);
    }
}

fn main() {
    let mut inv = Inventory { items: vec![] };

    // Can call read-only methods freely.
    println!("Count: {}", inv.count());

    // Must borrow mutably to call add.
    inv.add("Sword".to_string());
    println!("Count after add: {}", inv.count());
}

The count method takes &self. This is an immutable borrow of the whole struct. Callers can invoke count while other parts of the program hold references to the inventory. The add method takes &mut self. This demands exclusive access. Callers must ensure no other references exist before calling add.

Convention aside: &self signals the method is safe to call concurrently. &mut self signals it modifies state. This contract is part of your API design. Choosing &mut restricts how users can call your function. Choose & whenever possible to give users maximum flexibility.

Disjoint access: Rust is smarter than you think

Borrowing a field does not lock the entire struct. Rust tracks borrows at the field level. You can borrow one field immutably while mutating another.

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

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

    // Borrow x immutably.
    let x_ref = &p.x;

    // Mutating y is allowed. y is disjoint from x.
    p.y = 10;

    // Using x_ref is safe. x has not changed.
    println!("X is {}", x_ref);
}

The compiler sees that x and y occupy different memory locations. Mutating y cannot affect the value of x. The borrow checker allows this fine-grained access. This feature saves you from wrapping code in extra scopes just to drop a reference to one field.

Rust tracks fields individually. You get fine-grained control without the overhead of manual locking.

Pitfalls and compiler errors

The most common error is trying to create a mutable borrow while an immutable borrow is still alive.

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

    // Immutable borrow created here.
    let r1 = &s;

    // ERROR: E0502 cannot borrow `s` as mutable because it is also borrowed as immutable
    let r2 = &mut s;

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

The compiler rejects this with E0502. It sees r1 is still in scope when r2 tries to start. The mutable borrow would invalidate r1. The compiler prevents the conflict.

The fix is usually to drop the immutable borrow before the mutable one starts. Rust uses Non-Lexical Lifetimes. The compiler tracks where the reference is last used, not just where the variable goes out of scope. If you remove the println!, the code compiles because r1 is no longer used.

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

    let r1 = &s;
    // r1 is not used here. The compiler knows the borrow ends.

    // This compiles. r1 is dead.
    let r2 = &mut s;
    *r2 = "world".to_string();
}

Some types implement the Copy trait. For these types, &x behaves like a value copy in many contexts. The compiler automatically copies the data instead of creating a reference. This applies to integers, booleans, and floats. You rarely need to worry about borrows for i32. The borrow checker focuses on types that own memory, like String or Vec.

If the borrow checker blocks you, restructure your code. Isolating borrows in smaller scopes often resolves the conflict.

When compile-time checks fail

Sometimes you have a data structure where children need to mutate parents, or complex graphs where ownership is circular. The borrow checker cannot see the full picture. RefCell<T> allows interior mutability. It checks borrows at runtime. If you violate the rules, you get a panic.

use std::cell::RefCell;

fn main() {
    let data = RefCell::new(5);

    // Borrow mutably at runtime.
    let mut m = data.borrow_mut();
    *m = 10;

    // Drop the borrow explicitly.
    drop(m);

    // Now we can borrow immutably.
    let r = data.borrow();
    println!("{}", *r);
}

RefCell trades compile-time safety for runtime flexibility. Use it when the data structure is too complex for static analysis, not as a workaround for bad design. The community convention is to keep RefCell usage minimal. If you find yourself reaching for RefCell everywhere, your architecture likely needs refactoring.

RefCell moves the check to runtime. Use it when the structure demands it, not when the borrow checker is just being annoying.

Decision matrix

Use immutable borrows when you only need to read data and want to allow concurrent access from multiple parts of your code. Use mutable borrows when you need to modify data and can guarantee no other code is reading or writing at the same time. Use owned types when the data needs to outlive the current scope or when borrowing creates lifetime complexity that outweighs the cost of cloning. Reach for RefCell when you have a complex data structure with interior mutability that the borrow checker cannot verify statically.

Immutable borrows are free. Mutable borrows cost you flexibility. Pay that cost only when you must.

Where to go next