The iterator holds the map hostage
You're building a game leaderboard. The server just finished a round, and you need to award a bonus point to every player who participated. You grab your HashMap of scores, start looping through the entries, and try to bump the score for each player. The compiler stops you immediately. It refuses to let you mutate the map while you're reading it.
This isn't a bug. This is Rust protecting you from a memory safety disaster. The iterator holds an immutable borrow of the HashMap. That borrow prevents any mutable access until the loop finishes. You cannot update the map while the iterator is alive.
Why Rust blocks you
The iterator needs to read the internal structure of the HashMap to find the next key. It holds a reference to the map's buckets. If you mutate the map, the structure might change. Inserting a new key can trigger a resize. A resize allocates a new array of buckets, rehashes all existing entries, and frees the old array.
If the iterator is still pointing at the old array, it now holds a dangling pointer. Accessing it would be a use-after-free error. Rust prevents this by enforcing the borrow rules. The iterator is a reader. Mutation is writing. You cannot have a reader and a writer active at the same time.
The compiler rejects your code with E0502: cannot borrow as mutable because it is also borrowed as immutable. The error message tells you exactly what's wrong. The map is borrowed immutably by the loop. You're trying to borrow it mutably inside the loop. The borrows conflict.
The fix: collect keys first
The standard solution is to break the borrow before you mutate. Collect the keys you need into a separate collection, then iterate over that collection to update the map. The HashMap is no longer borrowed once the collection finishes. You can mutate it freely.
use std::collections::HashMap;
/// Awards a bonus point to every player in the scores map.
fn award_bonus(scores: &mut HashMap<String, i32>) {
// Collect keys into a Vec. This borrows the map immutably,
// but the borrow ends as soon as collect() returns.
let keys: Vec<_> = scores.keys().cloned().collect();
// Iterate over the owned keys. The map is free to mutate.
for key in keys {
if let Some(score) = scores.get_mut(&key) {
*score += 1;
}
}
}
fn main() {
let mut scores = HashMap::new();
scores.insert(String::from("Alice"), 10);
scores.insert(String::from("Bob"), 20);
award_bonus(&mut scores);
println!("{:?}", scores);
}
The keys() method returns an iterator of references to the keys. Calling cloned() converts those references into owned copies. The collect() call builds a Vec of owned keys. The Vec owns the data. The HashMap is untouched after the collection. You can now loop over the Vec and call get_mut on the map without conflict.
Walk through the borrow lifetimes
Understanding the lifetimes makes the pattern click. When you write for key in keys, the loop borrows the Vec immutably. The Vec is independent of the map. The map has no active borrows. You can call scores.get_mut safely.
If you skip the clone, the pattern fails. Collecting references keeps the borrow alive.
use std::collections::HashMap;
fn broken_update(scores: &mut HashMap<String, i32>) {
// BAD: keys holds references to the map.
// The immutable borrow of scores lives for the entire scope of `keys`.
let keys: Vec<_> = scores.keys().collect();
for key in keys {
// E0502: cannot borrow `scores` as mutable because it is also
// borrowed as immutable by `keys`.
if let Some(score) = scores.get_mut(key) {
*score += 1;
}
}
}
The keys variable holds &String values. Those references point into the HashMap. As long as keys exists, the map is borrowed immutably. You cannot mutate the map. You must clone the keys to break the link.
Realistic example: conditional updates
Real code rarely updates every entry. You usually have conditions. The collect-keys pattern works the same way. Collect the keys, filter inside the loop, and update only what matches.
use std::collections::HashMap;
/// Removes players with zero score and bumps everyone else.
fn cleanup_scores(scores: &mut HashMap<String, i32>) {
// Clone keys to allow mutation.
let keys: Vec<_> = scores.keys().cloned().collect();
for key in keys {
// Use get_mut to read and write atomically.
if let Some(score) = scores.get_mut(&key) {
if *score == 0 {
// Remove zero-score entries.
scores.remove(&key);
} else {
// Bump positive scores.
*score += 1;
}
}
}
}
fn main() {
let mut scores = HashMap::new();
scores.insert(String::from("Alice"), 0);
scores.insert(String::from("Bob"), 5);
scores.insert(String::from("Charlie"), 10);
cleanup_scores(&mut scores);
// Alice is gone. Bob is 6. Charlie is 11.
println!("{:?}", scores);
}
The if let Some pattern is safer than indexing. It handles the case where the key might have been removed. In this example, the key exists because you collected it from the map. However, using get_mut is a good habit. It signals to readers that you expect the key to exist but you're handling the borrow correctly.
Convention aside: use if let Some for updates. It's the idiomatic way to mutate a value in a HashMap. Indexing with scores[key] works for reading, but for writing, get_mut is clearer. It avoids creating a temporary reference that might confuse the borrow checker in complex expressions.
Pitfalls: the silent borrow
The most common mistake is forgetting to clone the keys. The compiler catches this with E0502, but the error can be confusing if you don't realize the Vec is holding the borrow. The error points to the mutation line, not the collection line. You might stare at the mutation and wonder why it's failing. The fix is always the same: ensure the keys are owned.
Another pitfall is performance. Cloning keys allocates memory. If your keys are large String values and your map has millions of entries, the Vec allocation can be expensive. You're duplicating all the keys in memory temporarily. For small maps, this cost is negligible. For huge maps, consider whether you can restructure the logic.
If you only need to update a subset of entries, filter the keys before collecting. This reduces the allocation size.
// Only collect keys that need updating.
let keys: Vec<_> = scores
.keys()
.filter(|k| k.starts_with("VIP_"))
.cloned()
.collect();
This pattern reduces the number of clones. The filter runs on the iterator, so it doesn't allocate. The collect happens after the filter, so you only clone the keys you need.
Performance and conventions
When keys implement Copy, use copied() instead of cloned(). It's a micro-optimization, but it's idiomatic. copied() works for types like u64, bool, or small structs. cloned() works for everything that implements Clone. The compiler optimizes both to the same machine code for Copy types, but copied() communicates your intent. It tells readers the key is cheap to duplicate.
Convention aside: prefer copied() for Copy keys. It's the community standard. It makes the code self-documenting.
If you're inserting new entries based on the iteration, the collect-keys pattern still works. Collect the keys, then insert new entries in the loop. The map can grow freely. The iterator is gone. The Vec of keys is independent. You can insert as many new keys as you want.
// Collect keys, then insert new entries.
let keys: Vec<_> = scores.keys().cloned().collect();
for key in keys {
// Insert a new entry derived from the old key.
scores.insert(format!("{}_bonus", key), 100);
}
This is safe. The new keys don't conflict with the iteration because the iteration is over. The map is fully mutable.
Decision matrix
Use keys().cloned().collect() when you need to mutate values for every entry and the map is small enough that a temporary vector of keys is cheap. Use keys().filter().cloned().collect() when you only need to update a subset of entries and want to minimize allocation. Use retain when you want to remove entries based on a condition while iterating, which handles the borrow internally. Use drain when you want to extract all values and empty the map, avoiding the allocation of a key list. Use the entry API when you are processing a single key and need to insert or update atomically without iterating the whole map.
Pick the tool that matches your mutation pattern. The borrow checker will thank you.