What Is the Entry API for HashMap in Rust?

The entry API in Rust HashMaps allows efficient insertion or modification of values based on key existence in a single call.

The single-lookup problem

You're building a leaderboard. A stream of events arrives: player names and points. You need to add points to existing players and create new entries for newcomers. In Python, you'd write scores[player] = scores.get(player, 0) + points. It's one line. It works.

In Rust, trying to replicate that line-by-line triggers the borrow checker. You can't look up a value and insert a new one in the same scope without juggling references. If you try to get a mutable reference to update a score, the map is borrowed mutably. You can't also insert a new key while that borrow is active. The compiler rejects the code with E0502 (cannot borrow as mutable because it is also borrowed as immutable) or a similar conflict.

The naive workaround involves two lookups. You check if the key exists. If it does, you mutate. If not, you insert. This hashes the key twice. It's slow. It's verbose. It fights the borrow checker.

The Entry API solves this. It gives you a handle to the map slot, letting you inspect and modify the value in a single operation. You hash the key once. The borrow checker stays happy. The code reads like the Python one-liner but compiles to efficient Rust.

The Entry handle

Think of the Entry API like a hotel concierge holding a reservation form. You hand the concierge a room number. The concierge checks the registry. If the room is empty, the concierge hands you a "Check In" form. If someone is staying there, the concierge hands you a "Guest Services" form.

In both cases, you get a form that lets you update the room status. You don't need to ask the registry twice. The concierge manages the access. The form is the Entry. It encapsulates the decision logic. You interact with the form, not the registry directly.

In Rust, map.entry(key) returns an Entry object. This object holds a mutable borrow of the map and the key you provided. It knows whether the key is present or absent. Methods like or_insert and and_modify operate on the Entry. They resolve the state and give you a mutable reference to the value.

Minimal example

use std::collections::HashMap;

/// Demonstrates basic entry API usage.
/// Shows insertion and mutation via a single handle.
fn main() {
    let mut scores = HashMap::new();

    // Insert "Blue" with 10 if missing.
    // If "Blue" exists, do nothing.
    // Returns a mutable reference to the value.
    scores.entry(String::from("Blue")).or_insert(10);

    // Insert "Red" with 5.
    scores.entry(String::from("Red")).or_insert(5);

    // Increment "Blue".
    // or_insert returns &mut V.
    // Dereference to mutate the value.
    *scores.entry(String::from("Blue")).or_insert(10) += 5;

    println!("{:?}", scores);
}

The entry method takes ownership of the key. If the key is missing, the map inserts the key you provided. If the key exists, the map drops the key you provided. The Entry object holds the borrow of the map. When or_insert runs, it matches on the internal state. If vacant, it inserts the value and returns a mutable reference. If occupied, it returns a mutable reference to the existing value.

The caller sees only &mut V. The caller doesn't know if the value was new or old. The Entry abstracts the branching logic.

Under the hood: The Entry enum

The Entry API works because of a clever enum structure. The entry method returns an Entry<'a, K, V>. This is an enum with two variants:

enum Entry<'a, K, V> {
    Occupied(OccupiedEntry<'a, K, V>),
    Vacant(VacantEntry<'a, K, V>),
}

The OccupiedEntry holds the existing value. The VacantEntry holds the key you passed and a slot to insert. Both variants hold a reference to the map. The lifetime 'a ties the Entry to the map's lifetime.

Methods like or_insert are defined on the Entry enum. They pattern match on the variants.

// Simplified logic of or_insert
impl<K, V> Entry<K, V> {
    fn or_insert(self, default: V) -> &'a mut V {
        match self {
            Entry::Occupied(entry) => entry.into_mut(),
            Entry::Vacant(entry) => entry.insert(default),
        }
    }
}

Both branches return &'a mut V. The type system guarantees the return type is the same regardless of the state. This is why the borrow checker accepts the code. The Entry owns the borrow. You're not borrowing the map twice. You borrowed it once to create the Entry, and the Entry holds the key to the kingdom.

The Entry API is a type-level state machine. It moves the branching logic from your code into the type system. You write linear code. The compiler generates the branches.

Realistic example: Word counter

A word counter is the classic use case. You need to count occurrences of words. You iterate over words. For each word, you increment the count. If the word is new, you start at zero.

use std::collections::HashMap;

/// Counts word frequencies in a text.
/// Uses entry API to avoid double lookups and borrow conflicts.
fn count_words(text: &str) -> HashMap<String, usize> {
    let mut counts = HashMap::new();

    for word in text.split_whitespace() {
        // Normalize word to lowercase.
        let key = word.to_lowercase();

        // Get mutable ref to count.
        // If key missing, insert 0 via closure.
        // or_insert_with avoids creating the 0 if key exists.
        let count = counts.entry(key).or_insert_with(|| 0);

        // Increment through the mutable reference.
        *count += 1;
    }

    counts
}

fn main() {
    let text = "Rust is fast. Rust is safe. Rust is fast.";
    let counts = count_words(text);
    println!("{:?}", counts);
}

The or_insert_with method takes a closure. The closure runs only if the key is missing. This prevents wasted computation. If the key exists, the closure never runs. The map returns the existing value.

Convention check: Use or_insert_with for expensive values. Use or_insert for cheap literals. The community treats or_insert_with as the safe default for anything slower than a literal. or_insert evaluates the argument eagerly. If you pass a function call, it runs even if the key exists.

// BAD: expensive_fn runs even if key exists.
map.entry(key).or_insert(expensive_fn());

// GOOD: closure runs only if key missing.
map.entry(key).or_insert_with(expensive_fn);

The and_modify chain

The Entry API supports a builder pattern. You can chain methods to handle both occupied and vacant cases in a single expression. The and_modify method runs a closure on the value if the key exists. It returns the Entry, allowing you to chain or_insert.

use std::collections::HashMap;

/// Updates scores using and_modify chain.
/// Demonstrates fluent entry API usage.
fn update_scores(mut scores: HashMap<String, u32>, player: String, points: u32) {
    // If key exists, increment the value.
    // If key missing, insert points as the initial value.
    scores
        .entry(player)
        .and_modify(|score| *score += points)
        .or_insert(points);
}

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

    update_scores(scores.clone(), String::from("Alice"), 5);
    update_scores(scores, String::from("Bob"), 20);

    println!("{:?}", scores);
}

The chain reads left to right. entry creates the handle. and_modify checks if occupied. If so, it runs the closure and returns the Entry. If not, it skips the closure and returns the Entry. or_insert checks if vacant. If so, it inserts the value. If occupied, it does nothing.

This pattern is common in Rust codebases. It keeps the logic compact. It avoids nested if let blocks. It makes the intent clear: modify if present, insert if absent.

Pitfalls: Key ownership and eager evaluation

The Entry API has two common traps. The first is key ownership. The entry method takes K by value. It consumes the key.

let mut map = HashMap::new();
let key = String::from("x");

// key is moved into entry.
map.entry(key).or_insert(1);

// key is no longer available.
// println!("{}", key); // Error: use of moved value

If you need the key after the entry operation, clone it before passing it.

let key = String::from("x");
let key_clone = key.clone();

map.entry(key).or_insert(1);

// key_clone is still valid.
println!("{}", key_clone);

The second trap is eager evaluation with or_insert. As noted, or_insert evaluates the argument immediately. If the value is expensive, you waste work.

fn compute_default() -> Vec<u8> {
    // Simulate expensive computation.
    (0..1000).collect()
}

let mut map = HashMap::new();
map.insert("x".to_string(), vec![1]);

// BAD: compute_default runs even though "x" exists.
// The returned Vec is dropped immediately.
map.entry("x".to_string()).or_insert(compute_default());

// GOOD: closure runs only if "x" is missing.
map.entry("x".to_string()).or_insert_with(compute_default);

Profile your code. If the default value is cheap, or_insert is fine. If it allocates, computes, or I/Os, use or_insert_with. The convention is to default to or_insert_with unless you're inserting a literal or a simple value.

Decision matrix

Use entry when you need to insert a default value if the key is missing and mutate the result in one step. Use entry with or_insert_with when the default value is expensive to compute and you want to avoid the cost if the key already exists. Use entry with and_modify when you need to update the existing value with logic that differs from a simple insert, chaining the modify and insert in a fluent expression. Reach for get_mut when you only care about existing keys and want to mutate them, skipping the insert logic entirely. Pick insert when you want to replace the value unconditionally, discarding the old one without reading it. Use get when you only need to read the value and don't plan to modify the map or the value.

The Entry API isn't just syntax sugar. It's the bridge between single-lookup efficiency and borrow checker compliance. Use it whenever you update a map based on key presence. Don't fight the compiler with double lookups. Reach for entry.

Where to go next