E0502

Rust E0502: Fixing Mutable and Immutable Borrow Conflicts

E0502 means you're trying to mutate data while an immutable reference still exists. The fix is usually to end the immutable borrow's scope before mutating.

You've written code that looks perfectly reasonable. You grab a reference to a value, then try to change it, and the compiler slaps you with error[E0502]: cannot borrow `foo` as mutable because it is also borrowed as immutable. It feels unfair. It's not. What's actually happening here is that Rust is enforcing the rule that lets it guarantee memory safety without a garbage collector: you can have many immutable references or one mutable reference to a value, never both at the same time.

The error usually shows up when you're iterating a collection and trying to add or remove items, when you call a method that returns a reference and then immediately try to modify the owner, or when you're holding a reference inside a struct while mutating another field. Let's take the most common cause first.

Cause 1: Iterating and mutating the same collection

This is the one that bites almost everyone at some point. You're looping over a Vec, a HashMap, or a BTreeMap, and inside the loop you try to push, insert, or remove. Here's a typical example:

let mut scores = vec![10, 20, 30];
for s in &scores {
    if *s == 20 {
        scores.push(99); // E0502 here
    }
}

The reason this fails isn't arbitrary. The for loop holds an immutable borrow of scores for the entire loop body. Pushing into the vector could reallocate its internal buffer, which would invalidate the reference s. Rust refuses at compile time rather than letting you find out at runtime.

The fix depends on what you're actually trying to do. If you're collecting new items based on old ones, build a separate vector and extend afterward:

let mut additions = Vec::new();
for s in &scores {
    if *s == 20 {
        additions.push(99);
    }
}
scores.extend(additions);

If you're removing items, use retain instead of looping and calling remove:

scores.retain(|&s| s != 20);

For a HashMap, the entry API exists precisely to avoid this class of error. Instead of checking if map.contains_key(&k) and then inserting, use map.entry(k).or_insert(default). That single method call does the lookup and the mutation in one atomic borrow.

The real fix is to stop treating iteration and mutation as a single operation. They're two operations. Splitting them costs you one extra Vec but the borrow checker stops complaining and your code gets clearer.

Cause 2: Holding a reference while calling a &mut self method

This pattern shows up constantly with structs. You have a method that borrows a field immutably, and then you call another method on self that takes &mut self. Example:

struct Cache {
    data: Vec<String>,
    hits: usize,
}

impl Cache {
    fn first(&self) -> &String {
        &self.data[0]
    }
    fn record_hit(&mut self) {
        self.hits += 1;
    }
}

fn main() {
    let mut c = Cache { data: vec![String::from("a")], hits: 0 };
    let first = c.first();
    c.record_hit(); // E0502: c is still borrowed by `first`
    println!("{}", first);
}

The compiler is right again. first is a reference into c.data, and record_hit mutates c, so the reference could be dangling if record_hit resized or reallocated. The fix is one of two things.

Clone the value if it's cheap and you don't need the borrowing semantics:

let first = c.first().clone();
c.record_hit();
println!("{}", first);

Or, if cloning is expensive, restrict the borrow's lifetime so it ends before the mutation:

let first_len = c.first().len(); // borrow ends on this line after NLL
c.record_hit();
println!("length was {}", first_len);

This works because of non-lexical lifetimes (NLL), which landed in Rust 2018 and became the default in Rust 1.31. Before NLL, a borrow lasted until the end of the enclosing block. Since NLL, a borrow ends at its last use. That's why the second version compiles: the compiler can see that first is never used after the len() call, so the immutable borrow is already over by the time record_hit runs.

If you're still on a pre-2018 edition crate for some reason, upgrade. There's no good reason to fight lexical borrows in 2024.

Cause 3: Match arms that borrow while you mutate in a guard or body

This one is sneakier. You've got a match on a reference, and inside an arm you try to mutate the thing you matched on. The borrow spans the entire match expression, so a mutation inside an arm collides with it.

enum State {
    Idle,
    Running(u32),
}

let mut state = State::Idle;
match &state {
    State::Idle => {
        state = State::Running(1); // E0502
    }
    State::Running(n) => {
        state = State::Running(n + 1); // E0502
    }
}

The fix is to extract the decision first, then mutate. Match on an owned or cloned value, or compute what you need and assign after the match:

let next = match state {
    State::Idle => State::Running(1),
    State::Running(n) => State::Running(n + 1),
};
state = next;

If the enum doesn't implement Copy but you don't need the value afterward, use std::mem::replace to take ownership temporarily:

let old = std::mem::replace(&mut state, State::Idle);
state = match old {
    State::Idle => State::Running(1),
    State::Running(n) => State::Running(n + 1),
};

mem::replace is underused. It lets you pull a value out of a mutable slot, leave a placeholder, transform the value, and put it back. No clone, no borrow conflict.

If you find yourself reaching for Rc<RefCell<T>> to silence E0502, stop and ask whether the data really needs shared mutable ownership. Nine times out of ten it doesn't, and the runtime borrow panics you'll get later are worse than the compile error you have now.

Quick reference

CauseSymptomFix
Mutating while iterating for x in &v { v.push(...) } Collect into a temp, use retain, or the entry API
Reference held across &mut self call Local binding returned from a borrow, then mutation Clone, or end the borrow before the mutation (NLL)
Mutating inside a match on a borrowed value Borrow spans all arms Compute the new value, assign after; or use mem::replace
Two fields, one borrowed one mutated Method returns &self.a, then &mut self.b Split borrows via a free function or destructure self
Closure captures immutably, then owner mutates let f = || &x; x.push(...) Scope the closure, or pass the data as a parameter

One last thing: if you're on Rust 2021 or later and still hitting E0502 in cases that look like they should work, check your function signatures. A method that returns &T from &self ties the returned reference's lifetime to self. Sometimes widening the return type — returning usize or an owned value instead of a reference — makes the borrow disappear without any restructuring. The borrow checker isn't smarter than you. It's just stricter, and often the fix is to give it less to check.

Related Errors in Programming & Dev Tools
0XC0000270 Fix STATUS_WX86_FLOAT_STACK_CHECK 0xC0000270 on Windows 0X0000022C Fix ERROR_UNEXPECTED_MM_CREATE_ERR 0X0000022C on Windows 0X4000001C STATUS_WX86_UNSIMULATE (0X4000001C) – What It Means 0X80000007 STATUS_WAKE_SYSTEM_DEBUGGER: Wake Reason and Fix

Was this solution helpful?

EP
Erropedia Team
Tech Support Editors
The Erropedia editorial team researches and documents real-world tech errors from across Windows, Linux, macOS, networking, databases, cloud platforms, and more. Every solution is reviewed for accuracy and updated as software and systems evolve.