Fix E0384: Cannot Borrow as Mutable Because `self` is Also Borrowed as Immutable

Rust intermediate linux macos windows webassembly

1. Symptoms

When attempting to compile Rust code that returns a mutable reference to self from a method that takes an immutable &self parameter, the compiler emits error E0384.

Typical error messages you’ll encounter:

error[E0384]: cannot borrow `self` as mutable because it is also borrowed as immutable
  --> src/main.rs:10:9
   |
9  |     fn get_mut(&self) -> &mut Self {
   |                      ----- first immutable borrow occurs here
10 |         &mut *self
   |         ^^^^^^^^^ second borrow occurs here due to use of `self` in the return type
   |
   = note: this error occurs in the generation of the v-table for trait implementation
   = note: the old error code E0501 has been superseded
---

```text
error[E0384]: cannot borrow `self` as mutable because it is also borrowed as immutable
  --> src/lib.rs:15:13
   |
14 |     fn push(&mut self, elem: T) -> &mut T {
   |             --------- first borrow occurs due to use of `self` in the return type
15 |         let elem_ref = &mut self.data[self.len];
   |                        ^^^^^^^^^^^^^^^^^^^^^ second borrow occurs here

The error specifically states that you cannot borrow self as mutable because it is already borrowed as immutable. This is a fundamental violation of Rust’s borrowing rules, which enforce that you cannot have both an immutable and mutable reference to the same data at the same time.

Additional symptoms include:

  • Methods that return &mut Self from &self implementations
  • Attempting to get mutable access to a field while returning a reference tied to &self
  • Nested method calls where the outer method holds an immutable borrow while calling a method expecting mutable access

2. Root Cause

The root cause of error E0384 is a lifetime/borrowing conflict where the compiler detects two overlapping borrows of self:

  1. The first borrow: The implicit immutable borrow of self from the method receiver (&self)
  2. The second borrow: The explicit mutable borrow attempted via &mut self or &mut *self in the return type

Rust’s borrowing rules state that:

  • You may have either one mutable reference or any number of immutable references
  • You cannot have both simultaneously
  • A mutable reference excludes all other references

Consider this problematic pattern:

struct Container {
    data: Vec<i32>,
}

impl Container {
    // This method signature itself creates the conflict
    fn get_mut(&self) -> &mut Vec<i32> {
        &mut self.data  // Compiler sees: immutable borrow `&self` + mutable borrow `&mut self.data`
    }
}

The method signature fn get_mut(&self) already creates an immutable borrow of self. When you then try to return &mut self.data, you’re attempting to create a mutable borrow that would coexist with the existing immutable borrow. This violates Rust’s aliasing rules.

Another common scenario involves method chaining:

impl Widget {
    fn update(&self) -> &mut Widget {
        self.inner = Inner::new();  // Cannot mutably borrow `self.inner` while `&self` is active
        &mut *self
    }
}

Here, &self creates an immutable borrow, and trying to access self.inner mutably (or return &mut *self) creates a conflicting mutable borrow.

3. Step-by-Step Fix

Fix 1: Change the method signature to accept &mut self

If you need to return a mutable reference to the object, the method must take a mutable receiver.

Before:

struct Counter {
    count: u32,
}

impl Counter {
    fn get_mut_ref(&self) -> &mut u32 {
        &mut self.count
    }
}

fn main() {
    let mut counter = Counter { count: 0 };
    let ref_to_count = counter.get_mut_ref();
    println!("{}", ref_to_count);
}

After:

struct Counter {
    count: u32,
}

impl Counter {
    fn get_mut_ref(&mut self) -> &mut u32 {
        &mut self.count
    }
}

fn main() {
    let mut counter = Counter { count: 0 };
    let ref_to_count = counter.get_mut_ref();
    println!("{}", ref_to_count);
}

Now the method takes &mut self, establishing a mutable borrow that can coexist with the mutable reference return type.

Fix 2: Return owned data instead of a reference

When you need to modify and return something, consider returning owned data rather than a reference.

Before:

struct Parser {
    buffer: String,
}

impl Parser {
    fn parse_header(&self) -> &mut String {
        &mut self.buffer
    }
}

After:

struct Parser {
    buffer: String,
}

impl Parser {
    fn parse_header(&mut self) -> String {
        // Return owned String, caller decides what to do with it
        let result = self.buffer.split_whitespace().next().unwrap_or("").to_string();
        result
    }
}

Fix 3: Use interior mutability with RefCell

For cases where you need shared ownership with mutation, use RefCell to move borrow checking to runtime.

Before:

struct State {
    value: i32,
}

impl State {
    fn modify(&self) -> &mut i32 {
        &mut self.value  // E0384
    }
}

After:

use std::cell::RefCell;

struct State {
    value: RefCell<i32>,
}

impl State {
    fn modify(&self) -> &mut i32 {
        self.value.borrow_mut()
    }
}

fn main() {
    let state = State { value: RefCell::new(42) };
    let result = state.modify();
    *result = 100;
    println!("{}", state.value.borrow());
}

⚠️ Unverified: Note that RefCell panics at runtime if you attempt nested mutable borrows. Ensure your usage pattern doesn’t create conflicting borrows.

Fix 4: Restructure the method to avoid returning &mut self

Before:

struct Buffer {
    data: Vec<u8>,
}

impl Buffer {
    fn flush(&self) -> &mut Buffer {
        // Process data...
        &mut *self
    }
}

After:

struct Buffer {
    data: Vec<u8>,
}

impl Buffer {
    fn flush(&mut self) -> &mut Buffer {
        // Process data...
        self
    }
}

fn main() {
    let mut buffer = Buffer { data: vec![1, 2, 3] };
    let buffer_ref = buffer.flush();
    println!("Flushed buffer with {} bytes", buffer_ref.data.len());
}

Fix 5: Split the operation into separate methods

Before:

struct Graph {
    nodes: Vec<Node>,
}

impl Graph {
    fn add_and_get(&self, node: Node) -> &mut Node {
        self.nodes.push(node);
        &mut self.nodes.last_mut().unwrap()
    }
}

After:

struct Graph {
    nodes: Vec<Node>,
}

impl Graph {
    fn add_node(&mut self, node: Node) {
        self.nodes.push(node);
    }

    fn get_last_node_mut(&mut self) -> Option<&mut Node> {
        self.nodes.last_mut()
    }
}

fn main() {
    let mut graph = Graph { nodes: Vec::new() };
    graph.add_node(Node { id: 1 });
    if let Some(node) = graph.get_last_node_mut() {
        println!("Last node ID: {}", node.id);
    }
}

4. Verification

After applying one of the fixes above, verify the error is resolved by compiling your project:

cargo build

A successful build produces:

   Compiling my_project v0.1.0 (file:///path/to/my_project)
    Finished dev [unoptimized + debuginfo] target(s) in 0.32s

For the RefCell approach, add a test to verify runtime borrow checking:

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_interior_mutability() {
        let state = State { value: RefCell::new(10) };

        // First mutable borrow
        let first = state.modify();
        *first = 20;

        // Second mutable borrow will panic at runtime
        // Uncomment to verify:
        // let second = state.modify();
        // *second = 30;

        assert_eq!(*state.value.borrow(), 20);
    }

    #[test]
    fn test_proper_mutable_receiver() {
        let mut counter = Counter { count: 5 };
        let ref_to_count = counter.get_mut_ref();
        *ref_to_count = 10;
        assert_eq!(counter.count, 10);
    }
}

Run the tests with:

cargo test

Expected output:

running 2 tests
test tests::test_interior_mutability ... ok
test tests::test_proper_mutable_receiver ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Additionally, verify that mutable borrows work correctly by testing actual mutation:

fn main() {
    // Test with mutable self receiver
    let mut counter = Counter { count: 0 };
    *counter.get_mut_ref() = 42;
    assert_eq!(counter.count, 42);

    // Test with RefCell
    let state = State { value: RefCell::new(100) };
    *state.modify() = 200;
    assert_eq!(*state.value.borrow(), 200);

    println!("All borrow patterns verified successfully!");
}

5. Common Pitfalls

Pitfall 1: Forgetting that trait methods have specific receiver requirements

When implementing traits, the method signature is often fixed. You cannot change &self to &mut self if the trait defines &self.

trait Serializable {
    fn serialize(&self) -> Vec<u8>;  // Cannot return &mut self
}

Fix: Implement a separate mutable method outside the trait, or redesign the API.

Pitfall 2: Confusing &self with self

New Rust developers sometimes think that because a method takes &self, they can return a mutable reference by copying/cloning. This only works for Copy types.

// This will NOT work for non-Copy types
impl Data {
    fn clone_data(&self) -> &Data {
        // Error: returning reference to local data
        // The temporary clone would be dropped immediately
        &Data { inner: self.inner.clone() }
    }
}

Pitfall 3: Attempting to pattern match around the borrow checker

impl Wrapper {
    fn try_get_mut(&self) -> Option<&mut Inner> {
        match self {
            Wrapper { inner } => Some(inner),  // Still borrows `self` immutably
        }
    }
}

You cannot use pattern matching to “extract” a mutable reference from an immutable borrow.

Pitfall 4: Nested borrows in closures

impl Cache {
    fn get_or_insert(&self, key: Key) -> &mut Value {
        if !self.map.contains_key(&key) {
            let value = Value::default();
            self.map.insert(key, value);  // Cannot mutably borrow while &self is active
        }
        self.map.get_mut(&key).unwrap()
    }
}

Pitfall 5: Inconsistent mutability across methods

struct Container {
    items: Vec<i32>,
}

impl Container {
    fn first(&self) -> &i32 {
        &self.items[0]
    }

    fn update_first(&self) -> &mut i32 {
        &mut self.items[0]  // E0384 - inconsistent with first()
    }
}

Both methods take &self, but one returns immutable and the other tries to return mutable. Make the mutable one take &mut self instead.

E0505: Cannot move out of self because it is borrowed

This error occurs when you try to move a value out of a struct while a reference to part of it still exists.

impl Foo {
    fn consume(self) -> Vec<i32> {
        self.data  // E0505: cannot move out of borrowed content
    }
}

E0506: Cannot assign to self because it is borrowed

When you have an immutable borrow of a field, you cannot assign to that same field.

struct Inner(u8);

impl Wrapper {
    fn update(&self, val: u8) {
        let _ref = &self.inner;  // Immutable borrow
        self.inner = Inner(val);  // E0506: cannot assign to `self.inner` because it is borrowed
    }
}

E0596: Cannot borrow self as mutable because it does not live long enough

Typically occurs when returning references that don’t outlive the borrow.

impl Parser {
    fn parse(&mut self) -> &[u8] {
        let result = &self.buffer[..];
        self.clear();  // Error if trying to clear and return
        result
    }
}

E0502: Cannot borrow self as mutable because it is also borrowed as immutable (same borrow region)

A stricter version of E0384 when the compiler detects the exact same borrow causing the conflict.

impl Widget {
    fn update(&mut self) {
        self.render();  // If render takes &self, this conflicts
        self.state = State::Updated;
    }
}