How to Merge Two HashMaps in Rust

Merge two Rust HashMaps by iterating over the second map and inserting its entries into the first using the Entry API.

Merging HashMaps: The Strategy Choice

You are building a configuration system for a game. The engine loads a defaults map with safe values: volume at 50, theme set to dark, language to English. Then the player loads their save file, which contains a user_prefs map. The save might have volume at 90 and language set to French, but it doesn't mention the theme. You need to combine these two maps into a single runtime config.

In Python, you reach for defaults.update(user_prefs) and move on. In JavaScript, you use the spread operator {...defaults, ...user_prefs}. Rust has no equivalent one-liner. The compiler stops you and asks a question that other languages hide: what happens when both maps contain the same key?

Rust refuses to guess. Merging strategies vary wildly. Sometimes the second map wins. Sometimes the first map is sacred. Sometimes you need to sum the values, or merge nested objects, or error out if a collision occurs. Because the strategy depends on your logic, Rust makes you write the merge explicitly. The mechanism is always the same: iterate over one map and insert its pairs into the other. You control the collision handling.

Rust forces you to define the merge strategy. There is no default because a hidden default is a bug waiting to happen.

The Minimal Merge: Overwrite

The most common merge is the "second map wins" strategy. You iterate over the source map and insert every key-value pair into the destination. If a key already exists, insert replaces the old value.

use std::collections::HashMap;

fn main() {
    // Base config with defaults.
    let mut config: HashMap<&str, i32> = HashMap::from([
        ("volume", 50),
        ("theme", 0),
        ("lang", 1),
    ]);

    // User overrides. Note: volume and lang collide.
    let user_overrides: HashMap<&str, i32> = HashMap::from([
        ("volume", 90),
        ("lang", 2),
        ("resolution", 1080),
    ]);

    // Iterate over user_overrides and insert into config.
    // insert() overwrites existing keys.
    for (key, value) in user_overrides {
        config.insert(key, value);
    }

    // config now has volume=90, lang=2, theme=0, resolution=1080.
    // user_overrides is consumed and no longer exists.
}

The loop uses user_overrides directly in the for statement. This calls into_iter(), which moves the keys and values out of the map. The map is destroyed in the process. This is efficient because you avoid cloning data. The values move into config where they belong.

If you need user_overrides after the merge, this code fails. The compiler rejects it with E0382 (use of moved value). You cannot iterate a map by value and keep using it. You must borrow instead, which changes the types you receive.

The insert method is the sledgehammer. It replaces blindly. Use it when the source of truth is the second map.

Keeping the Original: The Entry API

Sometimes the base map takes precedence. You want to fill in missing keys from the second map, but never overwrite what's already there. This is common for feature flags or cached values where the first write wins.

The entry API is the idiomatic tool for this. It gives you a handle to a specific key and lets you decide what to do based on whether the key exists.

use std::collections::HashMap;

fn main() {
    let mut cache: HashMap<&str, String> = HashMap::from([
        ("user_id", "123".to_string()),
        ("role", "admin".to_string()),
    ]);

    // Fallback values to fill gaps.
    let defaults: HashMap<&str, String> = HashMap::from([
        ("role", "guest".to_string()),
        ("theme", "light".to_string()),
    ]);

    // Merge defaults into cache, but only if the key is missing.
    for (key, value) in defaults {
        // entry() returns an Entry enum.
        // or_insert() adds the value only if the key is absent.
        cache.entry(key).or_insert(value);
    }

    // cache still has role="admin".
    // cache now has theme="light".
}

The entry(key) call looks up the key. or_insert(value) checks the result. If the key is missing, it inserts the value. If the key exists, it does nothing and drops the value you passed. This is safe and clear.

There is a performance benefit here too. A naive approach might check if !cache.contains_key(key) { cache.insert(key, value); }. That hashes the key twice: once for the check, once for the insert. The entry API hashes the key exactly once. It holds the insertion point open and lets you decide. The entry API pays for itself in hash lookups. Use it whenever you have conditional logic around insertion.

Realistic Merge: Combining Values

Real-world data rarely fits "overwrite" or "keep original". You often need to combine values. A word counter might merge two frequency maps by summing the counts. A stats aggregator might merge player scores by adding them together.

The entry API supports this with and_modify. You chain it after entry. If the key exists, and_modify runs a closure that lets you update the existing value. If the key is missing, you fall back to or_insert.

use std::collections::HashMap;

fn main() {
    // Word counts from the first half of a book.
    let mut counts: HashMap<&str, u32> = HashMap::from([
        ("rust", 15),
        ("safe", 8),
        ("fast", 12),
    ]);

    // Word counts from the second half.
    let second_half: HashMap<&str, u32> = HashMap::from([
        ("rust", 20),
        ("fast", 5),
        ("memory", 3),
    ]);

    // Merge by summing counts.
    for (word, count) in second_half {
        counts
            .entry(word)
            // If the word exists, add the new count to the existing one.
            .and_modify(|existing| *existing += count)
            // If the word is new, insert the count.
            .or_insert(count);
    }

    // counts: rust=35, safe=8, fast=17, memory=3.
}

The closure |existing| *existing += count receives a mutable reference to the value. You dereference it with * to perform the addition. This pattern scales to complex logic. You can merge nested maps, update timestamps, or apply validation rules inside the closure.

Use and_modify when the merge is a calculation, not a replacement. The logic lives right where the data is.

Pitfalls: Borrowing and Moving

The biggest trap in merging maps is confusing into_iter() with iter(). The examples above use into_iter() implicitly by iterating the map directly. This consumes the map. It is the most efficient path, but it destroys the source.

If you need to keep the source map, you must borrow it.

use std::collections::HashMap;

fn main() {
    let mut target: HashMap<&str, i32> = HashMap::from([("a", 1)]);
    let source: HashMap<&str, i32> = HashMap::from([("a", 2), ("b", 3)]);

    // source must survive this function.
    // Use iter() to borrow the map.
    for (key, value) in source.iter() {
        // iter() yields (&K, &V).
        // We need to clone the values to insert them into target.
        target.insert(key.clone(), value.clone());
    }

    // source is still usable here.
    println!("Source has {} entries", source.len());
}

Borrowing changes the types. iter() yields references. You get &&str and &i32 instead of &str and i32. You must clone the keys and values to insert them into the target. This costs memory and CPU. The compiler forces this trade-off. You cannot move data out of a borrowed map.

If you forget to clone, the compiler rejects the code with a type mismatch error. The types don't line up. insert expects owned values, but you handed it references. The compiler is protecting you from dangling pointers.

If you need the source map later, borrow it. The compiler will force your hand.

Convention: The Extend Trait

Rust provides a shortcut for the simple overwrite merge. The HashMap type implements the Extend trait. You can call extend on a map and pass an iterable of key-value pairs.

use std::collections::HashMap;

fn main() {
    let mut base = HashMap::from([("x", 1), ("y", 2)]);
    let extra = HashMap::from([("y", 3), ("z", 4)]);

    // extend() overwrites existing keys.
    // It consumes extra.
    base.extend(extra);

    // base: x=1, y=3, z=4.
}

extend is idiomatic for "dump everything in and overwrite". It is often faster than a manual loop because the compiler can optimize the trait implementation. It also handles other iterables, like vectors of tuples, without changing the call site.

The community treats extend as the "I don't care about collisions" merge. It is the equivalent of update in other languages. When you need custom logic, you drop down to the loop and entry.

extend is the fast path. Use it when you want to overwrite and move on. Reach for the loop when you need logic.

Decision Matrix

Merging maps is a choice of strategy. Pick the tool that matches your collision policy.

Use insert in a loop when you want the second map to overwrite the first and you are okay consuming the second map. Use entry().or_insert() when you want the first map to take precedence and fill gaps from the second map. Use entry().and_modify() when you need to combine values based on a custom rule, such as summing counts or merging nested structures. Use extend when you want a fast, overwrite-all merge and don't need custom collision logic. Use iter() instead of into_iter() when you need to keep the source map alive after the merge, accepting the cost of cloning keys and values.

Don't guess the collision strategy. Write it down. The code should make the policy obvious to the next reader.

Where to go next