Blog / Coding tips

JavaScript Map getOrInsert() and getOrInsertComputed(): The New Upsert Methods Explained

Cover image for JavaScript Map getOrInsert() and getOrInsertComputed(): The New Upsert Methods Explained

map.getOrInsert(key, defaultValue) returns the value stored under key; if the key isn’t in the map yet, it first inserts defaultValue and returns that. Its sibling, map.getOrInsertComputed(key, callback), does the same but only calls callback(key) to build the default when the key is actually missing. Together they replace the clumsy “check with has(), then set(), then get()” pattern that every JavaScript developer has written a hundred times. Both methods also exist on WeakMap.

These “upsert” methods are one of the newest additions to the language. The TC39 proposal reached Stage 4, MDN marks getOrInsert() as Baseline 2026, newly available since February 2026 across current browsers, and Node.js 26.0.0 (5 May 2026) shipped them through its V8 14.6 upgrade. Node 26 becomes the long-term support release this October, so many back-end developers are meeting these methods for the first time right now.

I ran every example below on Node.js 26.11.1 on 12 October 2026 and pasted the real output.

A quick Map refresher

If you’re new to Map, here are the basics the rest of this post builds on. A Map stores key–value pairs, remembers insertion order and can use any value as a key:

const m = new Map();
m.set("php", 3).set("js", 2);
console.log(m.get("php"), m.has("sql"), m.size);
m.delete("js");
console.log([...m.keys()], m.get("missing"));
3 false 2
[ 'php' ] undefined
  • set(key, value) adds or replaces an entry and returns the map, so calls can be chained.
  • get(key) returns the value, or undefined if the key is missing.
  • has(key) tells you whether the key exists.
  • delete(key) removes an entry, and size is the number of entries.

That undefined for a missing key is the root of the problem below: you constantly need to check for it and fill in a default.

The problem getOrInsert() solves

Say you want to count how often each word appears in a list. With a Map, the classic code looks like this:

const words = ["php", "js", "php", "sql", "php", "js"];
const counts = new Map();
for (const w of words) counts.set(w, (counts.get(w) ?? 0) + 1);
console.log(counts);
Map(3) { 'php' => 3, 'js' => 2, 'sql' => 1 }

That works, but it’s easy to get wrong, and grouping is worse. To group items into arrays you typically write:

if (!map.has(key)) map.set(key, []);
map.get(key).push(item);

That’s three lookups and two lines for one idea: “give me the array for this key, creating it if necessary.” Python has had dict.setdefault() for this for decades. JavaScript now has its own version.

getOrInsert(): basic usage

const m = new Map();
console.log(m.getOrInsert("a", 1), m.getOrInsert("a", 99), m);
1 1 Map(1) { 'a' => 1 }

The first call finds no "a", inserts 1 and returns it. The second call finds "a" already there, ignores the 99 and returns the existing 1. The map only ever holds one entry.

Grouping items in one line

Here’s the grouping pattern rewritten:

const posts = [{ cat: "php", t: "PDO" }, { cat: "js", t: "Temporal" }, { cat: "php", t: "clamp" }];
const byCat = new Map();
for (const p of posts) byCat.getOrInsert(p.cat, []).push(p.t);
console.log(byCat);
Map(2) { 'php' => [ 'PDO', 'clamp' ], 'js' => [ 'Temporal' ] }

This works because getOrInsert() returns the same array object that’s stored in the map, so pushing to it updates the map directly:

const arr = byCat.getOrInsert("php", []);
arr.push("x");
console.log(byCat.get("php"));
[ 'PDO', 'clamp', 'x' ]

Counting with getOrInsert()

For counting, getOrInsert() makes sure the key exists with a starting value of 0, and set() stores the new total:

const c2 = new Map();
for (const w of words) c2.set(w, c2.getOrInsert(w, 0) + 1);
console.log(c2);
Map(3) { 'php' => 3, 'js' => 2, 'sql' => 1 }

Numbers are copied rather than shared, so you still need the set(). The method saves you from the ?? 0 fallback and makes the intent clearer.

The trap: the default is evaluated every time

This is the most important thing to understand. JavaScript evaluates function arguments before the call, so the default value is created even when the key already exists and the value is thrown away. I counted how many arrays were created:

let made = 0;
const mk = () => { made++; return []; };
const g = new Map([["x", []]]);
g.getOrInsert("x", mk());
g.getOrInsert("x", mk());
console.log("arrays made:", made);
arrays made: 2

The key "x" existed both times, yet two arrays were built and discarded. For a small [] or 0, that doesn’t matter. For something expensive, such as a database query, a large object or a network call, it does. That’s exactly why there’s a second method.

getOrInsertComputed(): build the default only when needed

getOrInsertComputed() takes a callback instead of a value. It only calls the callback when the key is missing:

made = 0;
g.getOrInsertComputed("x", () => mk());
g.getOrInsertComputed("y", () => mk());
console.log("arrays made:", made, [...g.keys()]);
arrays made: 1 [ 'x', 'y' ]

"x" already existed, so no array was built. "y" was missing, so the callback ran once. The MDN page for getOrInsertComputed() describes the same rule: use it when the default is expensive to compute.

The callback receives the key

The callback gets the key as its argument, which makes a tidy memoisation cache:

const cache = new Map();
const sq = n => cache.getOrInsertComputed(n, k => { console.log("  computing", k); return k * k; });
console.log(sq(4), sq(4));
  computing 4
16 16

The square of 4 was computed once; the second call came straight from the cache. Swap k * k for a slow calculation and you have a simple, readable cache in one line.

Which one should you use?

  • getOrInsert(key, value): the default is a cheap literal such as 0, "", [] or {}.
  • getOrInsertComputed(key, fn): the default is expensive, has side effects, or depends on the key.

If in doubt, use getOrInsertComputed(). The extra arrow function costs almost nothing and avoids surprises.

A stored undefined counts as present

What if a key exists but its value is undefined? I tested it:

const u = new Map([["k", undefined]]);
console.log(u.getOrInsert("k", "default"), u.has("k"));
undefined true

The method checks whether the key exists, not whether the value is truthy, so it returns the stored undefined and doesn’t insert "default". That’s different from map.get(k) ?? "default", which would give you "default". If you need “missing or undefined”, keep the ?? approach.

WeakMap support

Both methods also exist on WeakMap, which is handy for attaching data to objects (such as DOM elements) without preventing them from being garbage-collected:

const wm = new WeakMap();
const obj = {};
console.log(wm.getOrInsertComputed(obj, () => ({ clicks: 0 })));
try { wm.getOrInsert("str", 1); } catch (e) { console.log(e.constructor.name + ":", e.message); }
{ clicks: 0 }
TypeError: Invalid value used as weak map key

WeakMap keys must be objects (or non-registered symbols), so a string key throws a TypeError, just as wm.set("str", 1) always has.

getOrInsert() vs Map.groupBy()

If you have the whole array up front and only want to group it once, Map.groupBy() (available since 2024 in modern engines) is even shorter:

console.log(Map.groupBy(posts, p => p.cat));
Map(2) {
  'php' => [ { cat: 'php', t: 'PDO' }, { cat: 'php', t: 'clamp' } ],
  'js' => [ { cat: 'js', t: 'Temporal' } ]
}

Use Map.groupBy() for a one-off grouping of an existing array. Use getOrInsert() when items arrive over time (events, stream chunks, rows from a paginated API) or when you’re adding to a map that already exists. For more array-processing patterns, my guide to JavaScript map, filter and reduce covers the basics these build on.

Why use a Map instead of a plain object?

You might wonder why not just use an object. For simple string keys, the modern object version of grouping is short too, thanks to the ??= operator:

const words = ["php", "js", "php"];
const obj = {};
for (const w of words) (obj[w] ??= []).push(w.length);
console.log(obj);
{ php: [ 3, 3 ], js: [ 2 ] }

But objects have limits that Maps don’t. Object keys are always strings or symbols, so you can’t key by an object or a number without it being converted. Objects also inherit properties: "toString" in {} is true, even though you never added it, which can confuse “does this key exist?” checks. Maps compare keys by identity, which I tested:

const m = new Map();
const a = { id: 1 }, b = { id: 1 };
m.getOrInsert(a, "first");
m.getOrInsert(b, "second");
console.log(m.size, m.get(a), m.get(b));
2 first second

a and b look identical but are different objects, so they get separate entries. Maps also treat NaN as equal to itself, so getOrInsert(NaN, ...) twice gives one entry:

const n = new Map();
console.log(n.getOrInsert(NaN, "nan"), n.getOrInsert(NaN, "again"), n.size);
nan nan 1

Counting characters: a quick exercise

Here’s a classic beginner exercise, finding the most common letters in a string, done with getOrInsert():

const counts = new Map();
"hello world".replace(/\s/g, "").split("").forEach(ch => counts.set(ch, counts.getOrInsert(ch, 0) + 1));
console.log([...counts].sort((x, y) => y[1] - x[1]).slice(0, 3));
[ [ 'l', 3 ], [ 'o', 2 ], [ 'h', 1 ] ]

Spreading a Map with [...counts] gives an array of [key, value] pairs that you can sort like any other array.

Checking support and adding a fallback

Feature-detect before relying on the methods:

console.log(typeof Map.prototype.getOrInsert, typeof Map.prototype.getOrInsertComputed);

On Node.js 26 this printed function function. On Node.js 20 it printed undefined. MDN’s “Baseline 2026” label means current versions of the major browsers support it, but older browsers and older Node versions don’t.

If you must support older runtimes, a small helper works everywhere and keeps the same semantics:

function getOrInsertComputed(map, key, fn) {
  if (map.has(key)) return map.get(key);
  const value = fn(key);
  map.set(key, value);
  return value;
}

Avoid patching Map.prototype yourself in application code. If you need a true polyfill, use a maintained one such as core-js, which checks for native support first.

Real-world examples

Building an index of tags to posts

const tagIndex = new Map();
for (const post of allPosts) {
  for (const tag of post.tags) tagIndex.getOrInsert(tag, new Set()).add(post.slug);
}

Using a Set as the default avoids duplicate slugs if a post lists the same tag twice.

Rate limiting by IP address

const hits = new Map();
function allow(ip, limit = 100) {
  const entry = hits.getOrInsertComputed(ip, () => ({ count: 0, since: Date.now() }));
  entry.count++;
  return entry.count <= limit;
}

The object is only created for new IPs. Real rate limiting also needs expiry, but the lookup itself is now one line.

Caching fetch promises

const requests = new Map();
const getUser = id => requests.getOrInsertComputed(id, () => fetch(`/api/users/${id}`).then(r => r.json()));

Storing the promise means two parts of the page asking for the same user at the same time share one network request. If you’re calling an external API this way, keep the keys on the server; see keeping an AI API key off the browser.

Where the name “upsert” comes from

“Upsert” is a database term that blends update and insert: update a record if it exists, otherwise insert it. The TC39 proposal that added these methods is called proposal-upsert, and its motivation section describes exactly the problem in this post: handling an update when you’re not sure whether the key already exists in a Map or WeakMap. The final design keeps it simple. The methods never overwrite an existing value; they only fill in missing ones and hand you back whatever is stored.

Common mistakes

  • Passing an expensive default to getOrInsert(). It’s built every time. Use getOrInsertComputed().
  • Forgetting set() when counting. Numbers aren’t shared by reference, so map.getOrInsert(k, 0) + 1 alone doesn’t update the map.
  • Expecting a stored undefined to be replaced. It won’t be; the key exists.
  • Using a string key on a WeakMap. WeakMap keys must be objects.
  • Assuming every user’s browser supports it. Check the version, or use the helper above.

FAQ

What does Map.getOrInsert() do in JavaScript?

It returns the value for a key if the key exists. If not, it inserts the default value you pass, then returns it. It replaces the common has()/set()/get() pattern with one call.

What is the difference between getOrInsert() and getOrInsertComputed()?

getOrInsert() takes a ready-made default value, which is evaluated on every call. getOrInsertComputed() takes a callback and only calls it, with the key as its argument, when the key is missing.

Is getOrInsert() supported in Node.js?

Yes, in Node.js 26 and later, which ships V8 14.6. Node.js 20 doesn’t have it. Check with typeof Map.prototype.getOrInsert === "function".

Which browsers support Map.getOrInsert()?

MDN marks it Baseline 2026, newly available since February 2026, meaning current versions of the major browsers support it. Older browser versions don’t, so feature-detect if you support them.

Does getOrInsert() work on WeakMap?

Yes. Both getOrInsert() and getOrInsertComputed() are available on WeakMap. Keys must still be objects or non-registered symbols.

Is getOrInsert() the same as Python’s setdefault()?

Very similar. Both return the existing value or insert and return a default. JavaScript also adds getOrInsertComputed(), which avoids creating the default when it isn’t needed.

Does getOrInsert() replace a stored undefined value?

No. It checks whether the key exists, so a key with an undefined value counts as present and undefined is returned.

// note

How to read this note.

This is a learning note from studying the web. It is one small topic, written so I can remember it. It is not a course and not a claim that I have finished the subject.

If a sentence is wrong, say so from the contact page and name this title. Drafts never appear here. Related notes, when they exist, are other published posts, and the same sample rule applies to each of them.

What Is msconfig? Windows System Configuration Explained for Beginners
Coding tips

What Is msconfig? Windows System Configuration Explained for Beginners

msconfig opens Windows System Configuration, a built-in troubleshooting tool for controlling how Windows starts. What each tab does, how to do a clean boot, diagnostic startup and safe boot, how to escape a safe mode loop, and why it matters for security.

October 12, 2026 · 12 min read · 1 view