Fix: cannot borrow as mutable because it is also borrowed as immutable (Rust E0502, E0382, E0505)

The Error

You compile your Rust code and the borrow checker stops you with one of these:

E0502 — mutable and immutable borrow conflict:

error[E0502]: cannot borrow `data` as mutable because it is also borrowed as immutable
 --> src/main.rs:5:5
  |
4 |     let r = &data;
  |             ----- immutable borrow occurs here
5 |     data.push(4);
  |     ^^^^^^^^^^^^ mutable borrow occurs here
6 |     println!("{}", r);
  |                    - immutable borrow later used here

E0382 — use of moved value:

error[E0382]: use of moved value: `s`
 --> src/main.rs:4:20
  |
2 |     let s = String::from("hello");
  |         - move occurs because `s` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = s;
  |              - value moved here
4 |     println!("{}", s);
  |                    ^ value used here after move

E0505 — moved while borrowed:

error[E0505]: cannot move out of `data` because it is borrowed
 --> src/main.rs:4:10
  |
3 |     let r = &data;
  |             ----- borrow of `data` occurs here
4 |     drop(data);
  |          ^^^^ move out of `data` occurs here
5 |     println!("{}", r);
  |                    - borrow later used here

All of these are the Rust compiler enforcing its ownership rules. The code won’t compile until you fix the conflict.

Why This Happens

Rust prevents data races and dangling references at compile time through three rules:

  1. Every value has exactly one owner. When the owner goes out of scope, the value is dropped.
  2. You can have either one mutable reference (&mut T) or any number of immutable references (&T) to a value — never both at the same time.
  3. References must always be valid. You can’t use a reference after the value it points to has been moved or dropped.

These rules are enforced by the borrow checker at compile time. There’s no runtime cost — it’s a zero-cost guarantee of memory safety.

The most common triggers:

  • Holding an immutable reference while trying to mutate. You read from a collection and try to modify it in the same scope.
  • Using a value after moving it. You pass a String, Vec, or other non-Copy type to a function or variable, then try to use the original.
  • Closures capturing variables. A closure borrows or moves a variable, preventing other uses of it.
  • Iterating and mutating. You loop over a collection and try to add or remove elements during the loop.

Fix 1: Limit Borrow Scope (Non-Lexical Lifetimes)

Since Rust 2018 (and the NLL — non-lexical lifetimes — update), the borrow checker tracks when a reference is last used, not when it goes out of scope. You can fix many conflicts by reordering your code so the borrow ends before the mutation.

Broken:

let mut data = vec![1, 2, 3];
let first = &data[0]; // immutable borrow starts
data.push(4);          // mutable borrow — conflict!
println!("{}", first); // immutable borrow used here

Fixed — use the reference before mutating:

let mut data = vec![1, 2, 3];
let first = &data[0];
println!("{}", first); // immutable borrow ends here (last use)
data.push(4);          // no conflict — immutable borrow is done

The key insight: a borrow lasts until its last use, not until the end of the block. Move the last use of the reference before the mutation and the conflict disappears.

Fix 2: Clone the Value

If you need the original value and a second copy, clone it. This gives you independent ownership of both.

Broken — move error:

let s = String::from("hello");
let s2 = s;           // s is moved
println!("{}", s);    // error: use of moved value

Fixed — clone:

let s = String::from("hello");
let s2 = s.clone();   // s2 is an independent copy
println!("{}", s);    // works — s still owns the original

When to clone:

  • When the data is small and cloning is cheap.
  • When you truly need two independent copies.
  • As a quick fix while you learn the ownership model.

When not to clone:

  • In hot loops with large data. Cloning a Vec<String> with a million entries is expensive.
  • When a reference (&T) would work just as well.

Clone is not a hack — it’s a legitimate tool. But if you find yourself cloning everything, you’re fighting the borrow checker instead of working with it.

Fix 3: Borrow Instead of Move

If a function only needs to read a value, take a reference instead of taking ownership.

Broken — function takes ownership:

fn print_length(s: String) {
    println!("Length: {}", s.len());
}

let s = String::from("hello");
print_length(s);
println!("{}", s); // error: use of moved value

Fixed — function borrows:

fn print_length(s: &str) {
    println!("Length: {}", s.len());
}

let s = String::from("hello");
print_length(&s);     // pass a reference
println!("{}", s);     // works — s was only borrowed

Rules of thumb:

  • Use &T when the function only reads the value.
  • Use &mut T when the function needs to modify the value.
  • Take T (ownership) only when the function needs to store the value or consume it.

String vs &str

A common source of confusion. String is an owned, heap-allocated string. &str is a borrowed reference to string data. Most functions that read strings should take &str, not String:

// Bad — forces the caller to give up ownership
fn greet(name: String) {
    println!("Hello, {}!", name);
}

// Good — accepts both &String and &str (via deref coercion)
fn greet(name: &str) {
    println!("Hello, {}!", name);
}

When you pass &String to a function expecting &str, Rust automatically converts it via deref coercion. No .clone() needed.

Fix 4: Split Borrows on Struct Fields

The borrow checker can track borrows of individual struct fields independently. If you’re borrowing the whole struct but only need specific fields, split the access.

Broken — borrowing the whole struct:

struct App {
    config: String,
    data: Vec<i32>,
}

impl App {
    fn process(&mut self) {
        let cfg = &self.config;     // immutable borrow of self
        self.data.push(42);          // mutable borrow of self — conflict!
        println!("{}", cfg);
    }
}

Fixed — borrow fields directly (no self method):

fn process(config: &str, data: &mut Vec<i32>) {
    data.push(42);
    println!("{}", config);
}

let mut app = App {
    config: String::from("prod"),
    data: vec![1, 2, 3],
};

process(&app.config, &mut app.data); // separate borrows — works

Alternative — restructure with helper methods:

impl App {
    fn config(&self) -> &str {
        &self.config
    }

    fn push_data(&mut self, value: i32) {
        self.data.push(value);
    }
}

The compiler can see that borrowing self.config and mutating self.data don’t conflict — but only when you access the fields directly on the struct, not through &self or &mut self methods. This is one of Rust’s known ergonomic rough edges.

Fix 5: Use .iter() vs .into_iter() Correctly

Iterating over a collection can either borrow or consume it. Pick the right method:

MethodWhat it doesAfter the loop
.iter()Borrows each element as &TCollection still usable
.iter_mut()Borrows each element as &mut TCollection still usable
.into_iter()Moves each element outCollection is consumed

Broken — into_iter consumes the Vec:

let names = vec![String::from("Alice"), String::from("Bob")];

for name in names {       // implicitly calls .into_iter()
    println!("{}", name);
}

println!("{:?}", names);  // error: use of moved value

Fixed — use .iter() to borrow:

let names = vec![String::from("Alice"), String::from("Bob")];

for name in &names {      // equivalent to names.iter()
    println!("{}", name);
}

println!("{:?}", names);  // works — names was only borrowed

The shorthand for item in &collection is equivalent to for item in collection.iter(). Use for item in &mut collection for mutable iteration.

Modifying elements during iteration

You can modify elements in place with .iter_mut():

let mut scores = vec![80, 90, 75];

for score in &mut scores {
    *score += 5; // dereference and modify
}

println!("{:?}", scores); // [85, 95, 80]

But you cannot add or remove elements while iterating. If you need to filter, collect into a new Vec:

let mut data = vec![1, 2, 3, 4, 5];
data = data.into_iter().filter(|x| x % 2 == 0).collect();
// data is now [2, 4]

Fix 6: Handle Closures and Captures

Closures capture variables from their environment. How they capture determines whether you can use the variable afterward.

Broken — closure moves the value:

let name = String::from("Alice");
let greet = move || println!("Hello, {}!", name);
println!("{}", name); // error: use of moved value

Fix A — don’t use move, let the closure borrow:

let name = String::from("Alice");
let greet = || println!("Hello, {}!", name); // borrows name
greet();
println!("{}", name); // works

Fix B — clone before the closure:

let name = String::from("Alice");
let name_copy = name.clone();
let greet = move || println!("Hello, {}!", name_copy);
println!("{}", name); // works — name was never moved

When you need move: You must use move when the closure outlives the current scope, such as when spawning threads or returning the closure from a function. Without move, the closure holds a reference that would dangle.

let name = String::from("Alice");

std::thread::spawn(move || {
    // name is moved into the thread — safe
    println!("Hello from thread: {}", name);
});

Fix 7: Use Option::as_ref() and Option::as_deref()

Calling .unwrap(), .map(), or pattern matching on an Option<String> or Option<Vec<T>> moves the inner value out of the option. Use .as_ref() or .as_deref() to borrow it instead.

Broken — match moves the String out:

let maybe_name: Option<String> = Some(String::from("Alice"));

if let Some(name) = maybe_name {
    println!("{}", name);
}

println!("{:?}", maybe_name); // error: use of moved value

Fixed — use .as_ref() to borrow:

let maybe_name: Option<String> = Some(String::from("Alice"));

if let Some(name) = maybe_name.as_ref() {
    println!("{}", name); // name is &String
}

println!("{:?}", maybe_name); // works

.as_deref() for &str:

let maybe_name: Option<String> = Some(String::from("Alice"));

// Converts Option<String> to Option<&str>
if let Some(name) = maybe_name.as_deref() {
    println!("{}", name); // name is &str
}

This pattern applies anywhere you match on or map over an Option or Result containing an owned type.

Fix 8: Use Rc<T> and Arc<T> for Shared Ownership

Sometimes multiple parts of your program genuinely need to own the same data. Use Rc<T> (reference counted, single-threaded) or Arc<T> (atomic reference counted, thread-safe).

Broken — can’t give two owners:

let data = vec![1, 2, 3];
let a = data;
let b = data; // error: use of moved value

Fixed — shared ownership with Rc:

use std::rc::Rc;

let data = Rc::new(vec![1, 2, 3]);
let a = Rc::clone(&data);
let b = Rc::clone(&data);

println!("{:?}", a); // [1, 2, 3]
println!("{:?}", b); // [1, 2, 3]

Rc::clone doesn’t deep-copy the data. It increments a reference count. The data is freed when the last Rc goes out of scope.

For multithreaded code, use Arc:

use std::sync::Arc;

let data = Arc::new(vec![1, 2, 3]);
let data_clone = Arc::clone(&data);

std::thread::spawn(move || {
    println!("{:?}", data_clone);
});

Arc is Rc with atomic operations, making it safe to share across threads. It’s slightly more expensive due to atomic reference counting.

Fix 9: Use RefCell<T> for Interior Mutability

Rc<T> gives shared ownership, but the data inside is immutable. If you need shared ownership and mutation, combine Rc with RefCell:

use std::cell::RefCell;
use std::rc::Rc;

let data = Rc::new(RefCell::new(vec![1, 2, 3]));
let data2 = Rc::clone(&data);

data.borrow_mut().push(4);   // mutable borrow at runtime
data2.borrow_mut().push(5);

println!("{:?}", data.borrow()); // [1, 2, 3, 4, 5]

RefCell moves borrow checking from compile time to runtime. If you violate the borrowing rules at runtime (e.g., two borrow_mut() calls at the same time), it panics instead of giving a compile error.

For multithreaded code, use Arc<Mutex<T>> instead:

use std::sync::{Arc, Mutex};

let data = Arc::new(Mutex::new(vec![1, 2, 3]));
let data_clone = Arc::clone(&data);

std::thread::spawn(move || {
    data_clone.lock().unwrap().push(4);
});

data.lock().unwrap().push(5);

Quick reference:

ScenarioUse
Single owner, no mutationT
Single owner, mutationmut T
Shared ownership, no mutationRc<T> / Arc<T>
Shared ownership + mutation (single-threaded)Rc<RefCell<T>>
Shared ownership + mutation (multi-threaded)Arc<Mutex<T>>

Fix 10: Add Lifetime Annotations

Sometimes the borrow checker can’t figure out how long references live. You need to tell it explicitly with lifetime annotations.

Broken — missing lifetime:

fn longest(a: &str, b: &str) -> &str {
    if a.len() > b.len() { a } else { b }
}
error[E0106]: missing lifetime specifier
 --> src/main.rs:1:35
  |
1 | fn longest(a: &str, b: &str) -> &str {
  |               ----     ----      ^ expected named lifetime parameter

Fixed — add lifetime annotation:

fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() > b.len() { a } else { b }
}

The 'a annotation tells the compiler: “the returned reference lives at least as long as the shorter of the two input references.” This lets the compiler verify that the caller doesn’t use the returned reference after either input is dropped.

When you don’t need lifetimes: Rust has lifetime elision rules that handle simple cases automatically. You only need explicit annotations when:

  • A function returns a reference and takes multiple reference parameters.
  • A struct stores a reference.
  • The compiler tells you to add them.

Struct with a reference:

struct Excerpt<'a> {
    text: &'a str,
}

let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().unwrap();
let excerpt = Excerpt { text: first_sentence };
// excerpt can't outlive novel — the lifetime guarantees this

If adding lifetimes feels complex, consider whether the struct should own the data (String instead of &str, Vec<T> instead of &[T]). Owned data is simpler to work with and avoids lifetime constraints entirely.

Still Not Working?

Read the full compiler error

Rust’s error messages are excellent. The error output shows you exactly where the conflicting borrows occur and often suggests a fix. Read the help: lines — they frequently contain the answer.

help: consider cloning the value if the performance cost is acceptable
  |
4 |     let r = data.clone();
  |                 ++++++++

Use cargo clippy for suggestions

Clippy catches common ownership anti-patterns:

cargo clippy

It may suggest using .as_ref(), removing unnecessary clones, or restructuring your borrows.

Temporary variables to shorten borrow lifetimes

If you’re borrowing a value from a complex expression, pull it into a temporary variable. This can give the borrow checker more information about when the borrow ends:

// Broken
let mut map = HashMap::new();
map.insert("key", vec![1, 2, 3]);
// This borrows map immutably (get) and mutably (insert) in one expression
if let Some(v) = map.get("key") {
    map.insert("key2", v.clone()); // error
}

// Fixed — extract the value first
let mut map = HashMap::new();
map.insert("key", vec![1, 2, 3]);
let cloned = map.get("key").cloned();
if let Some(v) = cloned {
    map.insert("key2", v); // works — immutable borrow of map ended
}

Use the entry API for HashMap

Many HashMap borrow conflicts come from checking if a key exists and then inserting. The entry API handles this in one step:

use std::collections::HashMap;

let mut counts: HashMap<&str, i32> = HashMap::new();
let words = vec!["hello", "world", "hello"];

for word in &words {
    // No borrow conflict — single operation
    *counts.entry(word).or_insert(0) += 1;
}

Shadowing to reclaim a variable name

If you need to transform a value and the original is no longer needed, shadow it:

let data = String::from("42");
let data: i32 = data.parse().unwrap(); // shadows the original String
// Original String is dropped, no move/borrow issues

Check if your type implements Copy

Types that implement Copy (integers, floats, bools, chars, tuples of Copy types, fixed-size arrays of Copy types) are duplicated on assignment instead of moved. If you’re getting move errors on a wrapper around a Copy type, consider deriving Copy:

#[derive(Clone, Copy)]
struct Point {
    x: f64,
    y: f64,
}

let a = Point { x: 1.0, y: 2.0 };
let b = a;           // copy, not move
println!("{:?}", a); // works

You can only derive Copy if all fields implement Copy. Types like String, Vec, and HashMap don’t implement Copy — they manage heap memory that can’t be cheaply duplicated.

Wrap mutable state in a newtype

If you’re fighting the borrow checker on a struct with many interdependent fields, consider extracting the mutable state into its own struct:

struct State {
    data: Vec<i32>,
    index: usize,
}

struct App {
    config: String,
    state: State,
}

impl App {
    fn process(&mut self) {
        // Borrow config immutably and state mutably — no conflict
        process_state(&self.config, &mut self.state);
    }
}

fn process_state(config: &str, state: &mut State) {
    state.data.push(42);
    println!("Config: {}, Index: {}", config, state.index);
}

Related: If you’re dealing with errors in other languages, see Fix: TypeError: Cannot read properties of undefined for a common JavaScript equivalent where values are unexpectedly absent. For dependency resolution issues in Go, see Fix: no required module provides package.