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 keyor_insert(value)— insertvalueif missing, otherwise return the existing oneor_insert_with(|| ...)— compute the default only when neededor_default()— insertDefault::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 lockerkeywithout 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.
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:
-
mapstarts empty.{} -
map.entry(key)asks the map for the entry at"foo". -
Since
"foo"is not present,or_default()insertsVec::default(), which is an empty vector.{"foo": []} -
valuesbecomes a mutable reference to that vector.&mut [] -
values.push(5)updates the stored vector inside the map.{"foo": [5]} -
values.push(8)updates it again.{"foo": [5, 8]} -
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.
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 borrowinsert()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:
defaultis 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
| Approach | Lookups | Inserts if missing | Borrow-friendly | Idiomatic Rust | Best use case |
|---|---|---|---|---|---|
get() + insert() manually | 1 or more | Yes | Often awkward | No | Rare, when logic is very custom |
contains_key() + get() | 2 | Yes | Safe | Not ideal | Simple code, but less efficient |
entry(...).or_insert(...) | 1 | Yes | Yes | Yes | Insert a known default |
entry(...).or_insert_with(...) | 1 | Yes | Yes | Yes | Default is expensive to build |
entry(...).or_default() | 1 | Yes | Yes | Yes | Type implements Default |
or_insert vs or_insert_with vs or_default
| Method | When value is created | Best for |
|---|---|---|
or_insert(value) | Always evaluated before call | Cheap default values |
| `or_insert_with( | value)` | |
or_default() | Only if key is missing | Types with a good Default, like Vec, String, HashMap |
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()returnsOption<&V>get_mut()returnsOption<&mut V>entry(key)gives access to the key slot directlyor_default()is great forVec,String,HashMap- avoid
contains_key()+get()whenentry()fits
Common types that work well with or_default()
Vec<T>→ empty vectorString→ empty stringHashMap<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.
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
HashMapwhere the key is a string slice and the value is aVec<i32>. - Add several numbers into categories such as
"even"and"odd". - Use the
entryAPI so missing categories are created automatically. - Print the final map contents.
- Print the values stored for one chosen category.
Keep learning
Related questions
Absolute Values in Rust: Using the abs() Method
Learn how to calculate absolute values in modern Rust with abs(), handle signed integer overflow, and choose safer alternatives.
Accepting Iterators in Rust Functions with impl Iterator
Learn how to write Rust functions that accept HashMap iterators using generic bounds or impl Iterator, avoiding trait object and lifetime errors.
Access Enum Variant Values in Rust with Pattern Matching
Learn how to extract and print values stored in Rust enum variants using match and if let pattern matching, with practical examples.