Question

Rust HashMap Entry API: Lookup or Insert Efficiently

rusthashmaplookup

Question

I want to retrieve a Vec from a Rust HashMap by key and keep a reference to it for later use. If the key does not exist, I want to create and insert an empty Vec for that key, then still use that inserted value through the same variable.

I first tried using match with map.get(key), like this:

use std::collections::HashMap;

let mut map: HashMap<&str, Vec<isize>> = HashMap::new();
let key = "foo";

let values: &Vec<isize> = match map.get(key) {
    Some(v) => v,
    None => {
        let default: Vec<isize> = Vec::new();
        map.insert(key, default);
        &default
    }
};

However, this does not compile because map.get(key) creates an immutable borrow, and then map.insert(...) tries to mutably borrow the same HashMap.

I then changed it to this, which works but performs two lookups:

use std::collections::HashMap;

let mut map: HashMap<&str, Vec<isize>> = HashMap::new();
let key = "foo";

if !map.contains_key(key) {
    map.insert(key, Vec::new());
}

let values: &Vec<isize> = match map.get(key) {
    Some(v) => v,
    None => panic!("this should be impossible")
};

Is there a safe and efficient Rust approach that does this in a single operation, ideally with one lookup?

Short Answer

By the end of this page, you will understand why borrowing rules make the original match map.get(...) approach fail, and how Rust's HashMap::entry API solves the problem cleanly. You will learn how to look up a value, insert a default if missing, and get a reference or mutable reference in one efficient operation.

Concept

Rust prevents you from having an immutable borrow and a mutable borrow of the same value at the same time. In your original code, map.get(key) immutably borrows the HashMap, and inside the None branch you try to call map.insert(...), which needs a mutable borrow. That combination is not allowed.

This matters because Rust uses borrowing rules to prevent bugs like:

  • reading stale references
  • invalidating references after mutation
  • data races in concurrent situations

For this exact pattern, Rust provides the Entry API on HashMap. The idea is simple:

  • check whether a key exists
  • if it exists, use the existing value
  • if it does not exist, insert one
  • return access to the value either way

This is both idiomatic and efficient because it avoids doing a separate contains_key and get lookup.

The most common methods are:

  • entry(key) — access an entry for a key
  • or_insert(value) — insert value if missing, otherwise return the existing one
  • or_insert_with(|| ...) — compute the default only when needed
  • or_default() — insert Default::default() if missing

For a Vec, or_default() is especially nice because an empty Vec is the default value.

Mental Model

Think of a HashMap like a wall of lockers.

  • get(key) means: "Open the building directory and look at locker key without changing anything."
  • insert(key, value) means: "Possibly add or change a locker."

Rust does not let you say: "While I am still in read-only inspection mode, also modify the lockers." That is what your original code tries to do.

The entry API is like asking the building manager: "Give me the locker for this key. If it doesn't exist, create it first." You make one request, and the manager handles both cases safely.

Take Quiz

Syntax and Examples

Core syntax

use std::collections::HashMap;

let mut map: HashMap<&str, Vec<isize>> = HashMap::new();
let values = map.entry("foo").or_default();

values is a &mut Vec<isize> here. If the key exists, you get a mutable reference to the existing vector. If not, Rust inserts an empty vector and gives you a mutable reference to that.

Example: lookup or insert

use std::collections::HashMap;

fn main() {
    let mut map: HashMap<&str, Vec<isize>> = HashMap::new();

    let values: &mut Vec<isize> = map.entry("foo").or_default();
    values.push(10);
    values.push(20);

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

Output:

{"foo": [10, 20]}

If you only need a shared reference afterward

Sometimes you mutate first, then later want an immutable reference:

use std::collections::HashMap;

fn main() {
    let mut map: HashMap<&str, Vec<isize>> = HashMap::new();
    let key = "foo";

    map.entry(key).or_default();

    let values: &Vec<isize> = map.get(key).unwrap();
    println!("{:?}", values);
}

Using or_insert directly

use std::collections::HashMap;

fn main() {
    let mut map: HashMap<&str, Vec<isize>> = HashMap::new();

    let values = map.entry("foo").or_insert(Vec::new());
    values.push(1);
}

This works, but or_default() is shorter for types like Vec that already implement Default.

Using or_insert_with for expensive defaults

use std::collections::HashMap;

fn main() {
    let mut map: HashMap<&str, Vec<i32>> = HashMap::new();

    let values = map.entry("foo").or_insert_with(|| {
        println!("Creating default vector");
        Vec::new()
    });

    values.push(42);
}

The closure only runs if the key is missing.

Step by Step Execution

Consider this code:

use std::collections::HashMap;

fn main() {
    let mut map: HashMap<&str, Vec<i32>> = HashMap::new();
    let key = "foo";

    let values = map.entry(key).or_default();
    values.push(5);
    values.push(8);

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

Step by step:

  1. map starts empty.

    {}
    
  2. map.entry(key) asks the map for the entry at "foo".

  3. Since "foo" is not present, or_default() inserts Vec::default(), which is an empty vector.

    {"foo": []}
    
  4. values becomes a mutable reference to that vector.

    &mut []
    
  5. values.push(5) updates the stored vector inside the map.

    {"foo": [5]}
    
  6. values.push(8) updates it again.

    {"foo": [5, 8]}
    
  7. Printing the map shows:

    {"foo": [5, 8]}
    

The important point is that you are not working with a separate temporary vector. You are working directly with the value stored inside the HashMap.

Real World Use Cases

This pattern appears constantly in real Rust programs.

Grouping items by key

use std::collections::HashMap;

let mut groups: HashMap<&str, Vec<i32>> = HashMap::new();

groups.entry("even").or_default().push(2);
groups.entry("even").or_default().push(4);
groups.entry("odd").or_default().push(3);

Useful for:

  • categorizing data
  • building indexes
  • aggregating records

Counting occurrences

use std::collections::HashMap;

let words = ["cat", "dog", "cat"];
let mut counts: HashMap<&str, usize> = HashMap::new();

for word in words {
    *counts.entry(word).or_insert(0) += 1;
}

Useful for:

  • analytics
  • logs
  • parsing text

Building adjacency lists in graphs

use std::collections::HashMap;

let mut graph: HashMap<&str, Vec<&str>> = HashMap::new();
graph.entry("A").or_default().push("B");
graph.entry("A").or_default().push("C");

Useful for:

  • dependency graphs
  • route maps
  • tree or graph processing

Caching computed results

You can use entry to create a cache slot only when needed.

Take Quiz

Real Codebase Usage

In real projects, developers often use the entry API as the standard way to avoid double lookups and awkward borrow issues.

Common pattern: accumulate values

use std::collections::HashMap;

fn add_tag(map: &mut HashMap<String, Vec<String>>, user: String, tag: String) {
    map.entry(user).or_default().push(tag);
}

This is common in:

  • request aggregation
  • event grouping
  • report generation

Common pattern: counters

use std::collections::HashMap;

fn count_event(counts: &mut HashMap<String, usize>, event: String) {
    *counts.entry(event).or_insert(0) += 1;
}

Guarding expensive construction

Use or_insert_with when creating the default is expensive:

use std::collections::HashMap;

fn get_buffer<'a>(buffers: &'a mut HashMap<String, Vec<u8>>, key: String) -> &'a mut Vec<u8> {
    buffers.entry(key).or_insert_with(|| vec![0; 1024])
}

Validation and initialization

Sometimes developers ensure a nested structure exists before filling it:

use std::collections::HashMap;

let mut user_roles: HashMap<String, Vec<String>> = HashMap::new();
user_roles.entry("alice".to_string()).or_default();

This is useful before later processing stages.

Nested maps

use std::collections::HashMap;

let mut data: HashMap<String, HashMap<String, i32>> = HashMap::new();
let inner = data.entry("group1".to_string()).or_default();
inner.entry("count".to_string()).or_insert(0);

This pattern appears in configuration systems, metrics collection, and structured aggregation.

Common Mistakes

1. Borrowing with get() and then trying to insert()

Broken code:

use std::collections::HashMap;

let mut map: HashMap<&str, Vec<i32>> = HashMap::new();
let key = "foo";

let values = match map.get(key) {
    Some(v) => v,
    None => {
        map.insert(key, Vec::new());
        map.get(key).unwrap()
    }
};

Why it fails:

  • get() creates an immutable borrow
  • insert() needs a mutable borrow
  • both overlap in the match

Fix:

let values = map.entry(key).or_default();

2. Returning a reference to a local temporary value

Broken code:

let values: &Vec<i32> = match map.get(key) {
    Some(v) => v,
    None => {
        let default = Vec::new();
        &default
    }
};

Why it fails:

  • default is destroyed when the block ends
  • the returned reference would point to invalid memory

Fix:

Insert the value into the map and borrow from the map itself using entry.

3. Using contains_key() and get() when one lookup is enough

This works, but is less efficient and less idiomatic:

if !map.contains_key(key) {
    map.insert(key, Vec::new());
}
let values = map.get(key).unwrap();

Better:

let values = map.entry(key).or_default();

4. Using or_insert(Vec::new()) when construction is expensive

or_insert(value) always evaluates value before the call.

let values = map.entry(key).or_insert(expensive_vec());

If expensive_vec() is costly, prefer:

let values = map.entry(key).or_insert_with(expensive_vec);

5. Forgetting that or_default() returns a mutable reference

let values = map.entry(key).or_default();

values is usually &mut Vec<_>, not &Vec<_>. That is often useful, but you should be aware of the type.

Comparisons

ApproachLookupsInserts if missingBorrow-friendlyIdiomatic RustBest use case
get() + insert() manually1 or moreYesOften awkwardNoRare, when logic is very custom
contains_key() + get()2YesSafeNot idealSimple code, but less efficient
entry(...).or_insert(...)1YesYesYesInsert a known default
entry(...).or_insert_with(...)1YesYesYesDefault is expensive to build
entry(...).or_default()1YesYesYesType implements Default

or_insert vs or_insert_with vs or_default

MethodWhen value is createdBest for
or_insert(value)Always evaluated before callCheap default values
`or_insert_with(value)`
or_default()Only if key is missingTypes with a good Default, like Vec, String, HashMap
Take Quiz

Cheat Sheet

Quick reference

use std::collections::HashMap;

Insert if missing, then get mutable reference

let value = map.entry(key).or_default();

Insert a specific default

let value = map.entry(key).or_insert(Vec::new());

Insert using lazy construction

let value = map.entry(key).or_insert_with(Vec::new);

Count items

*map.entry(key).or_insert(0) += 1;

Group values

map.entry(key).or_default().push(item);

Key points

  • get() returns Option<&V>
  • get_mut() returns Option<&mut V>
  • entry(key) gives access to the key slot directly
  • or_default() is great for Vec, String, HashMap
  • avoid contains_key() + get() when entry() fits

Common types that work well with or_default()

  • Vec<T> → empty vector
  • String → empty string
  • HashMap<K, V> → empty map
  • numeric types like usize → 0

Edge case

If you need an immutable reference instead of mutable access, you can still:

map.entry(key).or_default();
let value = map.get(key).unwrap();

FAQ

Why does map.get() followed by map.insert() fail in Rust?

Because get() immutably borrows the map, while insert() needs a mutable borrow. Rust does not allow those overlapping borrows.

What is the idiomatic Rust way to get-or-insert in a HashMap?

Use the entry API, usually with or_insert, or_insert_with, or or_default.

Should I use or_default() or or_insert(Vec::new())?

For a Vec, or_default() is usually cleaner and more idiomatic.

Does entry() do only one lookup?

Yes, that is one of its main benefits. It is designed for efficient get-or-insert behavior.

What type does or_default() return?

It returns &mut V, a mutable reference to the value in the map.

Can I use this pattern for counting values?

Yes. A common pattern is:

*map.entry(key).or_insert(0) += 1;

When should I use or_insert_with()?

Use it when building the default value is expensive or should happen only if the key is missing.

Can I still get an immutable reference after inserting?

Yes. You can insert with entry(...).or_default() first, then call get() afterward if needed.

Take Quiz

Mini Project

Description

Build a small Rust program that groups numbers by a label in a HashMap. This demonstrates the exact get or insert pattern that the entry API is designed for. It is a practical example because grouping values by category is common in data processing, logging, analytics, and report generation.

Goal

Create a program that stores numbers under category names and automatically creates a new Vec for a category the first time it is used.

Requirements

  • Create a HashMap where the key is a string slice and the value is a Vec<i32>.
  • Add several numbers into categories such as "even" and "odd".
  • Use the entry API so missing categories are created automatically.
  • Print the final map contents.
  • Print the values stored for one chosen category.
Take Quiz

Keep learning

Related questions

View more