← Debugging Guides

阅读中文版本

Rust — Cannot Borrow as Mutable More Than Once

Resolve E0499 and E0502 by shortening borrow scopes, restructuring data ownership, or using interior mutability only when runtime checking is truly required.

intermediatedebuggingrustborrow-checkerownership

Symptoms

  • error[E0499]: cannot borrow `value` as mutable more than once at a time
  • error[E0502]: cannot borrow `value` as immutable because it is also borrowed as mutable
  • The error often appears after adding a call inside a loop or holding a reference across an await

Possible causes

  • Two mutable references to the same value are alive at the same time
  • A mutable borrow is held while reading the same value
  • A borrow spans an await point or a loop iteration longer than intended
  • A method takes &mut self while a field reference is still in use

Diagnosis steps

Read the full borrow-checker error, including both spans

cargo check

Interpretation

The error shows the first borrow, the conflicting borrow, and the lifetime that keeps the first borrow alive. The longer span is usually the one to shorten.

Reduce the code to the smallest example

cargo check --message-format=short

Interpretation

A minimal reproducer makes the ownership relationship visible. Remove unrelated calls until only the conflicting borrows remain.

Inspect the borrow across loops, closures, and await points

cargo clippy --all-targets

Interpretation

Clippy often suggests a split borrow, a cloned value, or an iterator adjustment that resolves the conflict.

Root cause

Rust's aliasing rules allow either one mutable reference or any number of immutable references at a time. The code keeps a borrow alive longer than the compiler expects, so a second borrow overlaps with it.

Fix

Shorten the first borrow by computing a value, dropping the reference, or splitting the struct into independent fields. Restructure ownership so the mutating function owns the data, or use interior mutability such as RefCell or Mutex when runtime checking is genuinely required.

Risk note

Cloning to silence the borrow checker can change performance and semantics. RefCell panics at runtime on conflicting borrows and should not be used as a blind workaround.

Verify the fix

Run cargo test and cargo clippy with warnings denied. Confirm the original operation still mutates the intended value and add a test that exercises the borrow path, including concurrent access if a lock was introduced.

Find the overlapping lifetime

The compiler is not confused; it is showing two borrows that overlap. Read both spans and identify which one can end earlier. Often the fix is to compute a value, drop the reference, then borrow mutably.

// ❌ immutable borrow of items is alive during push
let first = items.first();
items.push(42);
println!("{first:?}");

// ✅ finish using the borrow before mutating
let first = items.first().copied();
items.push(42);
println!("{first:?}");

Structural fixes

  • Split a struct into fields so disjoint borrows are visible to the compiler.
  • Move mutation into a method that owns the data, returning a new value.
  • Use indices instead of references when iterating and mutating.
  • Use RefCell for single-threaded shared mutation and Mutex/RwLock for threads, accepting runtime overhead and possible blocking.

Verify

  • cargo check and cargo test pass.
  • cargo clippy -- -D warnings passes.
  • The mutation is visible where expected; no clone silently changed behavior.
  • If interior mutability was added, tests cover the borrow conflict and panic behavior.

Do not fight the compiler blindly

unsafe, clone() everywhere, and RefCell are legitimate tools in specific cases, but they do not explain the ownership model. Prefer restructuring borrows; use runtime checks only when the design truly requires shared mutation.