Collections5 min read

JavaScript Map getOrInsert Uses Key Presence, Not Truthiness

Use Map and WeakMap getOrInsert methods without replacing stored false, zero, empty-string, or undefined values.

  • Map
  • WeakMap
  • collections

Map.prototype.getOrInsert() returns the value for an existing key, even when that value is false, 0, '', null, or undefined. It inserts the supplied default only when the key is absent. getOrInsertComputed() uses the same presence rule but calls a function to produce the missing value.

The same two methods exist on WeakMap. All four are new in ECMAScript 2026’s keyed collections. Older runtimes may not implement them, so check all methods that an application needs rather than assuming that support for one implies support for every variant:

const supportsGetOrInsert =
  typeof Map.prototype.getOrInsert === 'function' &&
  typeof Map.prototype.getOrInsertComputed === 'function' &&
  typeof WeakMap.prototype.getOrInsert === 'function' &&
  typeof WeakMap.prototype.getOrInsertComputed === 'function';

console.log(supportsGetOrInsert);

Presence is different from a truthy value

A truthiness fallback cannot distinguish an absent key from a present key whose value happens to be falsy:

const preferences = new Map([
  ['animations', false],
  ['retryCount', 0],
  ['nickname', ''],
  ['accentColor', undefined],
]);

console.log(preferences.getOrInsert('animations', true));       // false
console.log(preferences.getOrInsert('retryCount', 3));          // 0
console.log(preferences.getOrInsert('nickname', 'Anonymous'));  // ''
console.log(preferences.getOrInsert('accentColor', 'blue'));    // undefined
console.log(preferences.getOrInsert('locale', 'en'));           // en

console.log(preferences.get('locale')); // en
console.log(preferences.size);          // 5

By contrast, map.get(key) || defaultValue replaces every falsy value, while map.get(key) ?? defaultValue replaces stored null and undefined. Neither expression inserts its result into the map.

The Map.prototype.getOrInsert algorithm searches for the key before appending a new entry. That is why a present undefined is still present. Before ECMAScript 2026, the equivalent test was map.has(key), not a check of map.get(key)’s value.

Choose eager or computed insertion

getOrInsert(key, value) receives an already evaluated value. Use it for cheap defaults such as numbers, strings, or a deliberately created object. getOrInsertComputed(key, callback) calls the callback only for an absent key, so it fits expensive or side-effecting construction.

const cache = new Map([['ready', { source: 'existing' }]]);
let creations = 0;

function createRecord(key) {
  creations += 1;
  return { key, source: 'created' };
}

cache.getOrInsert('ready', createRecord('ready'));
console.log(creations); // 1: function arguments are evaluated before the call

cache.getOrInsertComputed('ready', createRecord);
console.log(creations); // 1: callback was not called

console.log(cache.getOrInsertComputed('missing', createRecord));
// { key: 'missing', source: 'created' }
console.log(creations); // 2

The language’s argument-list evaluation rules explain the eager call. The computed Map algorithm passes the key as the callback’s only argument and stores the callback’s return value. If the callback returns undefined, that undefined is a valid inserted value.

One subtle validation rule applies even on a hit: getOrInsertComputed checks that its second argument is callable before it searches the map. Passing null or another non-function therefore throws even if the key is already present.

Incremental grouping does not require all input up front

An inserted array can become the accumulator for a group:

const messages = [
  { room: 'support', text: 'Hello' },
  { room: 'sales', text: 'Pricing?' },
  { room: 'support', text: 'Still there?' },
];

const byRoom = new Map();

for (const message of messages) {
  byRoom.getOrInsertComputed(message.room, () => []).push(message);
}

console.log(byRoom.get('support').map(({ text }) => text));
// ['Hello', 'Still there?']

This differs from Map.groupBy, which consumes an iterable and returns all groups as one eager operation. getOrInsertComputed is useful when records arrive incrementally or when other per-record aggregation happens in the same loop. It does not update an existing primitive value by itself; a counter still needs an explicit set:

const counts = new Map();

for (const word of ['red', 'blue', 'red']) {
  counts.set(word, counts.getOrInsert(word, 0) + 1);
}

console.log([...counts]); // [['red', 2], ['blue', 1]]

Callback errors and reentrancy remain visible

If a computed callback throws, the method propagates that exception and does not perform its own insertion. JavaScript does not roll back other mutations that the callback completed before throwing:

const records = new Map();

try {
  records.getOrInsertComputed('report', () => {
    records.set('audit', 'attempted');
    throw new Error('report unavailable');
  });
} catch (error) {
  console.log(error.message); // report unavailable
}

console.log(records.has('report')); // false
console.log(records.get('audit'));  // attempted

The callback can also modify the same key. The ECMAScript 2026 algorithm deliberately searches again after the callback returns. If the callback inserted that key, the method replaces its value with the callback’s return value:

const themes = new Map();

const result = themes.getOrInsertComputed('editor', (key) => {
  themes.set(key, 'side effect');
  return 'computed';
});

console.log(result);               // computed
console.log(themes.get('editor')); // computed

Keep default callbacks small and avoid mutating the same collection from inside them unless this overwrite behavior is intentional. The method prevents two entries for one key; it does not make a multi-step callback transactional.

WeakMap has the same value rules and narrower keys

The ECMAScript 2026 WeakMap variants use the same eager and computed value contracts. They are useful for metadata whose lifetime follows its key:

const metadata = new WeakMap();
const element = {};

const first = metadata.getOrInsertComputed(element, () => ({ reads: 0 }));
first.reads += 1;

const second = metadata.getOrInsertComputed(element, () => ({ reads: 0 }));
console.log(first === second); // true
console.log(second.reads);     // 1

WeakMap keys must be values that can be held weakly: objects and non-registered symbols qualify, while strings, numbers, and symbols created by Symbol.for() do not. The WeakMap insertion algorithms throw TypeError for an invalid key before running the computed callback.

These methods solve one narrow operation: return the existing value or establish a default. Use getOrInsert when the default is already available, and getOrInsertComputed when construction should happen only on a miss. In both cases, reason about key presence rather than the stored value’s truthiness.