DEV Community

JKC
JKC

Posted on

Stop Writing if (!map.has(key)). Map Has getOrInsert()

TL;DR: map.getOrInsert(key, []) returns the value for key, and adds [] first if the key is missing. getOrInsertComputed() does the same with a callback, so the default is only built when it is needed. Both landed on Map and WeakMap, and they are Baseline 2026 Newly available.

The three lines everyone has typed

You have a Map and you want to group things into it. So you write the ritual:

if (!groups.has(key)) {
  groups.set(key, []);
}
groups.get(key).push(item);
Enter fullscreen mode Exit fullscreen mode

Three lookups to add one item. Or the shorter version that sneaks in a falsy bug the day someone stores 0:

const list = groups.get(key) || groups.set(key, []).get(key);
Enter fullscreen mode Exit fullscreen mode

It works. It also looks like a riddle.

Every codebase has a getOrCreate helper for this, usually in a file called utils.js, next to three other helpers nobody remembers writing.

What it is

The TC39 "Upsert" proposal reached Stage 4 at the January 2026 meeting, and it adds two methods to both Map and WeakMap:

  • getOrInsert(key, defaultValue) returns the existing value for key. If the key is missing, it stores defaultValue and returns that.
  • getOrInsertComputed(key, callback) does the same, but calls callback(key) only when the key is missing, and stores what it returns.

That is the whole API. No options object, no flags.

A minimal example

Counting words, the classic:

const counts = new Map();

for (const word of ["css", "js", "css", "html", "css"]) {
  counts.set(word, counts.getOrInsert(word, 0) + 1);
}

console.log([...counts]);
// [["css", 3], ["js", 1], ["html", 1]]
Enter fullscreen mode Exit fullscreen mode

And the grouping ritual from above becomes one line:

const orders = [
  { id: 1, customer: "ana" },
  { id: 2, customer: "ben" },
  { id: 3, customer: "ana" },
];

const byCustomer = new Map();
for (const order of orders) {
  byCustomer.getOrInsert(order.customer, []).push(order.id);
}

console.log([...byCustomer]);
// [["ana", [1, 3]], ["ben", [2]]]
Enter fullscreen mode Exit fullscreen mode

Both outputs are from a run in Chrome 155.

When the default is expensive

getOrInsert() is a normal function call, so its arguments are evaluated before it runs. If the default is costly, you pay for it even when the key already exists:

let built = 0;
const make = () => {
  built++;
  return 42;
};

const cache = new Map([["a", 1]]);

cache.getOrInsert("a", make());
console.log(built); // 1, built for nothing

built = 0;
cache.getOrInsertComputed("a", make);
console.log(built); // 0, key exists, callback skipped

cache.getOrInsertComputed("b", make);
console.log(built); // 1, key missing, callback ran once
Enter fullscreen mode Exit fullscreen mode

Rule of thumb: a literal like 0 or [] goes to getOrInsert(). Anything that does real work goes to getOrInsertComputed().

The callback also receives the key, which keeps lazy caches short:

const upper = new Map();
console.log(upper.getOrInsertComputed("hello", (key) => key.toUpperCase()));
// "HELLO"
Enter fullscreen mode Exit fullscreen mode

A realistic example: per-element state in a WeakMap

The same methods exist on WeakMap, which is where they get quietly useful. Say you want to track state for DOM elements without leaking memory when they are removed:

const state = new WeakMap();

function stateFor(element) {
  return state.getOrInsertComputed(element, () => ({ clicks: 0 }));
}

document.addEventListener("click", (event) => {
  const button = event.target.closest("button");
  if (!button) return;

  const s = stateFor(button);
  s.clicks++;
  button.dataset.clicks = s.clicks;
});
Enter fullscreen mode Exit fullscreen mode

The first click creates the state object, every later click reuses it, and when the button leaves the DOM the entry can be garbage collected. In a Chrome test page, clicking button A three times and a <span> inside button B once left data-clicks="3" and data-clicks="1", with the callback running exactly twice.

Can I use it?

From MDN's compatibility data:

Runtime Version
Chrome / Edge 145
Firefox 144
Safari 26.2
Node.js 26.0.0

MDN marks it Baseline 2026, Newly available since February 2026. That means current browsers are fine, but anything older needs a fallback or a polyfill, since the methods simply do not exist there.

Gotchas

  1. Node 24 does not have it. On Node 24.11 (V8 13.6), typeof Map.prototype.getOrInsert is "undefined". If your code also runs on the server, check your Node version before you delete that helper.

  2. An existing undefined counts as existing. new Map([["x", undefined]]).getOrInsert("x", 5) returns undefined and leaves it there. The check is "does the key exist", not "is the value truthy", which is exactly what the || trick gets wrong.

  3. The callback's return value wins. If the callback itself calls map.set(key, ...), the method still stores what the callback returns. In a run, the map ended up holding "returned", not the value set inside the callback, with a size of 1. Better still, do not mutate the map from inside the callback.

  4. WeakMap keys still must be objects. new WeakMap().getOrInsert("str", 1) throws TypeError: Invalid value used as weak map key. The new methods do not relax the old rules.

  5. The error message blames the wrong thing. Pass a non-function to getOrInsertComputed() and Chrome 155 throws TypeError: Map.prototype.getOrInsertComputed is not a function. The method exists. It is your second argument that is not a function.

Wrap-up

The has, set, get dance was always three lookups wearing a trench coat. Find your getOrCreate helper, swap it for getOrInsert() or getOrInsertComputed(), and delete one more file from utils/.

Sources

This article was written with the help of AI and checked against the sources above.

Top comments (0)