JavaScript's Map finally has a built-in way to say "give me this key, and create it if it's missing," and you should reach for getOrInsertComputed() almost every time. Its sibling getOrInsert() looks simpler, but it builds the default value on every call, even when the key already exists.
Both methods are Baseline 2026 and work in every current major browser. By the end of this post you'll know which one to use, the one pattern where the computed version quietly saves you network requests, and the cases where neither one is the right tool.
The pattern you've written a hundred times
Grouping things into buckets is one of the most common jobs a Map does. A Map is a key/value collection where keys can be any value, including objects, and insertion order is kept. Here's the usual way to group orders by customer.
const byCustomer = new Map();
for (const order of orders) {
if (!byCustomer.has(order.customer)) {
byCustomer.set(order.customer, []);
}
byCustomer.get(order.customer).push(order);
}
It works. It also looks the key up twice on a hit and three times on a miss, and it spreads one idea across four lines. People shorten it with map.get(key) ?? [], but that version breaks the moment null or undefined is a legitimate stored value, and you still need a separate set().
The new methods
There are two of them, on both Map and WeakMap.
getOrInsert(key, defaultValue) returns the value for key if it exists. If it doesn't, it stores defaultValue under that key and returns it.
getOrInsertComputed(key, callback) does the same thing, except the default comes from a function. The function only runs when the key is missing, and it receives the key as its one argument.
Here's the grouping loop again.
const byCustomer = new Map();
for (const order of orders) {
byCustomer.getOrInsertComputed(order.customer, () => []).push(order);
}
One line, one idea, and the returned value is the actual array stored in the map, so push() mutates the right thing.
Why the computed version should be your default
JavaScript evaluates function arguments before the function runs. That's true for every call, and it's the whole story here. When you write map.getOrInsert(key, buildSomething()), buildSomething() runs first, every single time. The method then throws the result away if the key was already there.
let calls = 0;
function buildIndex(key) {
calls++;
return { key, entries: [] };
}
const eager = new Map();
eager.getOrInsert("home", buildIndex("home"));
eager.getOrInsert("home", buildIndex("home"));
console.log(calls); // 2
calls = 0;
const lazy = new Map();
lazy.getOrInsertComputed("home", buildIndex);
lazy.getOrInsertComputed("home", buildIndex);
console.log(calls); // 1
With an empty array literal the waste is tiny. A throwaway [] costs almost nothing. The trouble is that code changes. Today the default is []. Six months from now someone swaps it for new Set(loadSeedData()) or a parsed config object, and now every lookup pays for work it discards. Nothing errors, and nothing looks wrong in review. It just gets slower.
My rule is simple. Use getOrInsert() when the default is a primitive like 0 or "". Use getOrInsertComputed() for everything else. A counter is the clean case for the first one.
const counts = new Map();
for (const word of words) {
counts.set(word, counts.getOrInsert(word, 0) + 1);
}
Caching promises to skip duplicate requests
Because the callback's return value is stored as is, an async callback stores a promise. That turns out to be really useful. Two parts of a page asking for the same URL at the same moment get the same in-flight request instead of firing two.
const requests = new Map();
function getJSON(url) {
return requests.getOrInsertComputed(url, async (key) => {
const response = await fetch(key);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
});
}
I ran this with two simultaneous calls for the same URL. One fetch went out, and both callers got the identical object back.
There's a cost, and it's the part people miss. A rejected promise stays in the map too. If the network blips once, every later call for that URL gets the same failure forever. If you cache promises this way, delete the entry when it fails so the next call can try again.
function getJSONWithRetry(url) {
const pending = getJSON(url);
pending.catch(() => requests.delete(url));
return pending;
}
The map also never shrinks on its own. For a long-lived page that requests lots of unique URLs, you want a real cache with limits, not a bare Map.
WeakMap for per-element state
WeakMap got the same two methods. A WeakMap only accepts objects (and non-registered symbols) as keys, and it doesn't keep those keys alive. When a DOM element is removed and nothing else references it, its entry can be garbage collected. That makes it a good fit for state you hang off elements in widgets or browser extension content scripts.
const widgetState = new WeakMap();
function stateFor(element) {
return widgetState.getOrInsertComputed(element, () => ({ open: false, clicks: 0 }));
}
Pass a string as a WeakMap key and you get a TypeError, same as set() would give you.
One edge case worth knowing
If the callback itself writes to the same key, the callback's return value wins. The spec looks the key up again after the callback runs and overwrites whatever is there. I'd treat that as a smell. A callback that builds a default shouldn't also be editing the map it's building for.
When not to use it
Support is the main cost. MDN's compatibility data lists Chrome and Edge 145, Firefox 144, and Safari 26.2. Server code is a different story. Node.js only has these methods starting in version 26, so anything still running on Node 22 or 24 will throw TypeError: byCustomer.getOrInsertComputed is not a function. Shared utility code that runs in both places needs a polyfill from core-js or es-shims until your oldest runtime catches up.
It's also not a replacement for Object.groupBy() or Map.groupBy() when you have the whole array up front and only need to group it once. Those already hand you finished groups in one call. The new methods earn their place when entries arrive over time, like streamed records or lookups scattered across a codebase.
Wrap-up
When a missing key needs a default, call getOrInsertComputed() and pass a function. Save getOrInsert() for primitive defaults, delete rejected promises if you cache them, and check your Node version before you put it in shared code.