How to Iterate Over a HashMap in Rust

Iterate over a Rust HashMap using a for loop with .iter() to access key-value pairs.

When you need to process every entry

You have a HashMap full of data. Maybe it's a cache of parsed configs, a leaderboard of player scores, or a registry of open network connections. You need to process every entry. You reach for a for loop. Rust gives you options, but picking the wrong one can trigger borrow checker errors or accidentally move data you still need. Iterating over a HashMap is simple once you know which iterator method matches your intent.

How HashMap iteration works

A HashMap stores key-value pairs in buckets. The position of each pair depends on the hash of the key. When you iterate, you visit these buckets. The order you see the items is determined by the hash function and the internal bucket layout, not by insertion order. If you need sorted keys, a HashMap is the wrong tool.

The iteration methods return iterators. An iterator is a type that produces items one by one. Most HashMap iterators yield references to the keys and values, not the values themselves. This avoids copying data and respects ownership. You get (&K, &V) pairs. If you need to change values, you use a mutable iterator. If you need to take ownership, you consume the map.

The iterator borrows the HashMap. The lifetime of the iterator is tied to the map. You cannot hold a reference to a key or value longer than the map itself lives. This rule prevents dangling pointers. If you try to extend the lifetime, the compiler rejects you.

Minimal example

The most common pattern is iterating over references to keys and values. Use a for loop with a reference to the map. The compiler desugars this to .iter().

use std::collections::HashMap;

/// Demonstrates basic iteration over a HashMap.
fn main() {
    let mut scores = HashMap::new();
    scores.insert(String::from("Alice"), 100);
    scores.insert(String::from("Bob"), 200);
    scores.insert(String::from("Charlie"), 150);

    // &scores calls .iter() automatically.
    // The loop yields (&String, &u32) pairs.
    for (name, score) in &scores {
        // name is &String, score is &u32.
        // Dereference score to print the integer value.
        println!("{name}: {score}");
    }
}

Convention aside: for (k, v) in &map is preferred over for (k, v) in map.iter(). Both do the same thing. The reference syntax is idiomatic and shorter. The community treats the explicit .iter() call as redundant noise in a for loop.

Walkthrough of the borrow

When you write for (name, score) in &scores, the compiler rewrites this. It calls .iter() on the reference. The iter() method returns an iterator that yields (&K, &V). The loop unpacks the tuple. The references point directly into the HashMap's internal storage. No cloning happens.

The HashMap stays alive for the duration of the loop because the references borrow it. The iterator holds an immutable borrow on the map. If you try to insert or remove items inside this loop, the borrow checker rejects you. The map is borrowed immutably by the iterator. You cannot mutate a collection while iterating over it with a shared borrow.

This protection exists because mutation can invalidate the internal structure. Inserting an item might trigger a resize. A resize allocates a new bucket array and moves entries. Any references to the old entries become dangling pointers. The borrow checker prevents this class of bugs at compile time.

Realistic example: filtering and transforming

In real code, you rarely just print. You filter, transform, or accumulate. You can chain iterator methods to build pipelines. This avoids intermediate allocations and keeps the code declarative.

use std::collections::HashMap;

/// Calculates bonuses for high-scoring players.
/// Returns a new HashMap with doubled scores for players above the threshold.
fn calculate_bonus(scores: &HashMap<String, u32>) -> HashMap<String, u32> {
    let threshold = 150;
    let mut bonuses = HashMap::new();

    // Iterate and filter in one pass.
    // .iter() yields (&String, &u32).
    for (name, score) in scores {
        // Dereference score to compare with the threshold.
        if *score > threshold {
            // Clone the key to own it in the new map.
            // The original map keeps its keys.
            bonuses.insert(name.clone(), score * 2);
        }
    }

    bonuses
}

fn main() {
    let mut scores = HashMap::new();
    scores.insert(String::from("Alice"), 100);
    scores.insert(String::from("Bob"), 200);
    scores.insert(String::from("Charlie"), 150);

    let bonuses = calculate_bonus(&scores);

    // Print the results.
    for (name, bonus) in &bonuses {
        println!("{name} gets a bonus of {bonus}");
    }
}

You can also use functional chaining. The .filter() method takes a closure that returns a boolean. The .map() method transforms items. The .collect() method gathers results into a collection. This style is concise but can be harder to debug if the closure logic is complex.

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    scores.insert(String::from("Alice"), 100);
    scores.insert(String::from("Bob"), 200);

    // Filter and collect into a Vec of tuples.
    // The closure receives (&String, &u32).
    let high_scores: Vec<(&String, &u32)> = scores
        .iter()
        .filter(|(_, &score)| score > 150)
        .collect();

    for (name, score) in high_scores {
        println!("{name} scored {score}");
    }
}

Variants: keys, values, and mutation

Sometimes you only need keys. .keys() returns an iterator of &K. .values() returns &V. This is slightly more efficient because the iterator doesn't construct tuples. Use these methods when you don't need the other half of the pair.

use std::collections::HashMap;

fn main() {
    let mut config = HashMap::new();
    config.insert("timeout".to_string(), 30);
    config.insert("retries".to_string(), 3);

    // Iterate only over keys.
    // Yields &String.
    for key in config.keys() {
        println!("Key: {key}");
    }

    // Iterate only over values.
    // Yields &i32.
    for value in config.values() {
        println!("Value: {value}");
    }
}

If you need to modify values in place, use .iter_mut(). This yields (&K, &mut V). You can update the value without removing and re-inserting. This is useful for batch updates.

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    scores.insert("Alice".to_string(), 100);
    scores.insert("Bob".to_string(), 200);

    // Iterate with mutable access to values.
    // Yields (&String, &mut u32).
    for (name, score) in scores.iter_mut() {
        // Double the score in place.
        *score *= 2;
    }

    // Verify the changes.
    for (name, score) in &scores {
        println!("{name}: {score}");
    }
}

If you need to take ownership of the data, use into_iter(). This consumes the HashMap and yields (K, V). The map is gone after the loop. This is useful when you're moving data into a new structure and don't need the map anymore.

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    scores.insert(String::from("Alice"), 100);
    scores.insert(String::from("Bob"), 200);

    // into_iter() consumes the map.
    // Yields (String, u32).
    let owned_entries: Vec<(String, u32)> = scores.into_iter().collect();

    // scores is moved and cannot be used here.
    // println!("{:?}", scores); // Error: use of moved value.

    for (name, score) in owned_entries {
        println!("{name}: {score}");
    }
}

Pitfalls and compiler errors

Common mistake: trying to modify the map while iterating. for (k, v) in &map { map.insert(...) } fails with E0502. You cannot borrow as mutable while borrowed as immutable. The iterator holds an immutable borrow. The insert requires a mutable borrow. The compiler rejects this to prevent iterator invalidation.

Solution: collect changes and apply them after the loop. Or use the retain method to remove items based on a predicate. Or use the entry API for complex updates.

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    scores.insert("Alice".to_string(), 100);
    scores.insert("Bob".to_string(), 200);

    // Collect items to remove.
    let mut to_remove = Vec::new();
    for (name, score) in &scores {
        if *score < 150 {
            to_remove.push(name.clone());
        }
    }

    // Apply removals after iteration.
    for name in to_remove {
        scores.remove(&name);
    }
}

Another pitfall: assuming order. HashMap order is random. If you print and expect alphabetical, you're wrong. Use BTreeMap for order. The BTreeMap maintains keys in sorted order and iteration yields items in that order. The trade-off is slightly slower insertion and lookup due to tree balancing.

Cloning keys unnecessarily is a performance trap. for (k, v) in map.iter() { let owned_k = k.clone(); }. If you only need a reference, don't clone. Cloning allocates memory. If you need to store the key elsewhere, clone it. If you just need to read it, use the reference.

Performance considerations

Iterating a HashMap is O(N) in time, where N is the number of entries. However, the memory layout hurts cache performance. HashMap entries are scattered across buckets. The iterator jumps around in memory as it visits buckets. A Vec iteration walks through contiguous memory, which is much faster for the CPU cache.

If you iterate over a HashMap frequently in a tight loop, consider copying the keys or values into a Vec first. The allocation cost of the Vec is often paid back by faster iteration due to cache locality. This optimization matters in hot paths. For occasional iteration, the overhead of copying isn't worth it.

Decision matrix

Use .iter() when you need to read both keys and values without taking ownership. Use .keys() when you only need the keys and want to avoid tuple overhead. Use .values() when you only need the values. Use .iter_mut() when you need to update values in place without removing entries. Use into_iter() when you need to take ownership of keys and values and discard the map. Use BTreeMap when you need iteration in sorted key order.

Don't fight the borrow checker here. Collect your mutations and apply them after the loop. HashMap order is random. If order matters, you picked the wrong map. Trust into_iter() to clean up. It consumes the map and frees the memory.

Where to go next