Error E0502

"cannot borrow as mutable because it is also borrowed as immutable" — How to Fix

Resolve Rust error E0502 by separating immutable and mutable borrows into distinct scopes to prevent simultaneous access.

The borrow checker blocks your mutation

You're writing a function to update a configuration map. You grab a reference to a value to check its current state. Then you try to insert a new entry. The compiler rejects the code with E0502. You have an immutable borrow active, and you're asking for a mutable one. Rust says no.

This error isn't a bug in your logic. It's a guarantee about memory safety. Rust enforces a rule: you can have many readers, or one writer, but never both at the same time. E0502 fires when you violate this rule. You hold a reference that promises "I won't change this," and then you ask for a reference that says "I might change this." The compiler blocks the second request to prevent undefined behavior.

Aliasing and mutability: the rule behind the error

Rust prevents two dangerous conditions simultaneously: aliasing and mutability. Aliasing means two references point to the same data. Mutability means the data can be changed.

Imagine a whiteboard in a meeting. One person is reading the notes to understand the plan. Another person wants to erase a section and rewrite it. If the reader is still looking, the writer can't change the board without confusing the reader. The reader might see half-erased text or a mix of old and new information.

Rust applies this to memory. If you allow a mutable reference while an immutable reference exists, the immutable reference might read garbage, partial updates, or cause a data race in multi-threaded code. E0502 is the compiler's way of enforcing that the whiteboard is either being read by many people or written by one person, never both.

The error message reads "cannot borrow as mutable because it is also borrowed as immutable." It tells you exactly what is wrong: a mutable borrow overlaps with an active immutable borrow. Your job is to find the overlap and break it.

Minimal example: the vector reallocation trap

The most common trigger for E0502 involves vectors. Vectors store data on the heap. When you push a new element, the vector might need more space. If the current capacity is full, the vector allocates a larger block of memory, copies the data, and frees the old block.

If you hold a reference to an element while pushing, that reference points to the old memory. After the push, the reference becomes a dangling pointer. Rust stops you before this happens.

fn main() {
    let mut numbers = vec![10, 20, 30];

    // Borrow immutable: we promise not to change `numbers`.
    // This reference points to the first element in the heap allocation.
    let first = &numbers[0];

    // Borrow mutable: we want to change `numbers`.
    // `push` might reallocate the vector, invalidating `first`.
    // The compiler sees the overlap and rejects this with E0502.
    numbers.push(40);

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

The compiler rejects this with E0502. It sees first holding a reference to numbers[0]. The mutable borrow for push starts before first is done. The overlap is clear. Even if push doesn't reallocate in this specific run, the compiler must assume it might. It enforces the rule for all possible executions.

Why the compiler stops you: lifetimes and NLL

The borrow checker tracks lifetimes. A lifetime is the scope during which a reference is valid. In older Rust, lifetimes ended at the closing brace of the scope. Modern Rust uses Non-Lexical Lifetimes (NLL). With NLL, a borrow ends at the last use of the reference, not the end of the scope.

This helps reduce false positives. If you use a reference, then mutate the data, then use the reference again, the compiler sees the gap and allows it. E0502 only fires when the last use of the immutable borrow overlaps with the mutable borrow.

fn main() {
    let mut numbers = vec![10, 20, 30];

    let first = &numbers[0];

    // Use the immutable borrow here.
    println!("First value: {}", first);

    // `first` is no longer used after the println.
    // NLL ends the immutable borrow here.
    // The mutable borrow can start safely.
    numbers.push(40);
}

This code compiles. The last use of first is the println. The mutable borrow starts after that. No overlap. Trust NLL. If you're getting E0502, the overlap is real. Look for a use of the immutable reference that extends past the mutation.

Realistic example: the HashMap check-and-insert

A common pattern in real code is checking if a key exists in a map, and inserting a default value if it doesn't. A naive implementation often triggers E0502.

use std::collections::HashMap;

struct Cache {
    data: HashMap<String, String>,
}

impl Cache {
    /// Returns a reference to the value for the key.
    /// Inserts a default value if the key is missing.
    fn get_or_insert(&mut self, key: &str, default: &str) -> &str {
        // Immutable borrow: check if the key exists.
        // This borrows `self.data` immutably.
        let existing = self.data.get(key);

        if existing.is_none() {
            // Mutable borrow: insert the default value.
            // This borrows `self.data` mutably.
            // Conflict: `existing` still holds a reference to `self.data`.
            // E0502 fires here.
            self.data.insert(key.to_string(), default.to_string());
        }

        // Return a reference to the value.
        self.data.get(key).unwrap()
    }
}

The problem is existing. It holds a reference to self.data. Even though you check is_none(), the reference is still alive. The compiler doesn't track the logic inside the if. It sees the reference and the mutation in the same scope.

The idiomatic fix is the entry API. It handles the check and insert in one step, avoiding the borrow conflict.

use std::collections::HashMap;

struct Cache {
    data: HashMap<String, String>,
}

impl Cache {
    /// Returns a reference to the value for the key.
    /// Inserts a default value if the key is missing.
    fn get_or_insert(&mut self, key: &str, default: &str) -> &str {
        // `entry` takes a mutable borrow of `self.data`.
        // It returns an `Entry` enum that represents the slot.
        // The `Entry` handles the borrow internally.
        let entry = self.data.entry(key.to_string());

        // `or_insert` inserts the default if missing.
        // It returns a mutable reference to the value.
        let value = entry.or_insert_with(|| default.to_string());

        // Return a reference to the value.
        // The `Entry` is dropped, releasing the mutable borrow.
        value
    }
}

The entry API works because it encapsulates the mutation. You don't hold a reference to the map while mutating it. The Entry object manages the state. Convention aside: the community treats entry as the standard solution for check-then-act patterns on HashMap. It avoids double lookups and sidesteps borrow conflicts. Use it.

Pitfalls and hidden borrows

E0502 can be tricky when borrows are hidden inside closures or iterators.

Closures capture references. If you pass a closure to a function, and the closure captures a reference, that reference stays alive for the duration of the closure. If the function mutates the data, E0502 fires.

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

    // Closure captures an immutable reference to `data`.
    let reader = || println!("{}", data[0]);

    // `reader` is called here, but the borrow might extend.
    reader();

    // If `reader` is used later, the borrow extends.
    // This mutation might conflict with the closure's capture.
    data.push(4);
}

Iterators can also hold borrows. If you create an iterator over a collection, the iterator holds a reference. Mutating the collection while the iterator exists triggers E0502.

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

    // Iterator holds an immutable borrow of `items`.
    let iter = items.iter();

    // Mutation conflicts with the iterator's borrow.
    items.push(4);
}

The fix is to consume the iterator or finish the iteration before mutating. Or use methods that don't hold borrows across mutations.

Tools to resolve E0502

When you hit E0502, you have several tools to break the overlap.

Splitting borrows works for slices and arrays. If you need to mutate two disjoint parts of a slice, you can't borrow the whole slice twice. Use split_at_mut to get two mutable slices.

fn main() {
    let mut buffer = [1, 2, 3, 4];

    // Split the slice into two mutable parts.
    // `left` covers indices 0..2.
    // `right` covers indices 2..4.
    // The compiler knows they don't overlap.
    let (left, right) = buffer.split_at_mut(2);

    // Mutate both parts safely.
    left[0] = 10;
    right[1] = 40;
}

Cloning works when the data is small. If you need to hold a snapshot while mutating the original, clone the value. This breaks the reference chain.

fn main() {
    let mut config = String::from("debug");

    // Clone the value to break the borrow.
    let snapshot = config.clone();

    // Mutate the original.
    config.push_str("_mode");

    // Use the snapshot.
    println!("Old config: {}", snapshot);
}

Convention aside: cloning is cheap for small types like String or small vectors. It's expensive for large data structures. Clone only when the data is small and the logic demands it. Don't clone everything to silence the borrow checker.

Interior mutability is the escape hatch. RefCell<T> allows mutation through an immutable reference. It moves the borrow check to runtime. If you violate the rules, the program panics.

use std::cell::RefCell;

struct State {
    value: RefCell<i32>,
}

impl State {
    fn update(&self) {
        // Borrow mutable through an immutable reference.
        // This works at compile time.
        // It panics at runtime if the borrow is already active.
        let mut val = self.value.borrow_mut();
        *val += 1;
    }
}

Convention aside: the community treats RefCell as a signal that the data structure design might be fighting the borrow checker. Use it when compile-time borrows are too rigid, but accept the runtime overhead and panic risk. RefCell is for single-threaded code. For multi-threaded code, use Mutex<T>.

Decision matrix

Use separate scopes when the read and write operations are logically distinct and can be ordered sequentially. Ensure the immutable borrow ends before the mutable borrow begins.

Use cloning when the data is small and you need to hold a snapshot while mutating the original. Clone the value to break the reference chain.

Use the entry API when you are checking for existence and potentially inserting into a HashMap. It avoids double lookups and handles borrows internally.

Use split_at_mut when you need to mutate two disjoint parts of a slice simultaneously. It proves to the compiler that the parts don't overlap.

Use RefCell<T> when you need interior mutability and cannot restructure the code to satisfy the borrow checker. Accept the runtime overhead and panic risk.

Use Arc<Mutex<T>> when you need shared ownership and mutation across threads. RefCell is not thread-safe.

Where to go next