Back to blog

Map.getOrInsert ES2026: Finally Native Upsert in JavaScript

Hello HaWkers, one of the most practical additions in ES2026 finally solves a problem every JavaScript developer has faced: checking if a key exists in a Map before inserting. The new methods getOrInsert and getOrInsertComputed arrive in Chrome 145 in January 2026.

No more if (!map.has(key)) { map.set(key, value); }. Let's see how to use it.

The Current Problem

Why we need upsert.

Repetitive Pattern

Code everyone writes:

// BEFORE: Grouping items by category
const groups = new Map();

for (const item of items) {
  if (!groups.has(item.category)) {
    groups.set(item.category, []);
  }
  groups.get(item.category).push(item);
}

// Or the "clever" but confusing version:
for (const item of items) {
  const arr = groups.get(item.category) ?? [];
  if (!groups.has(item.category)) {
    groups.set(item.category, arr);
  }
  arr.push(item);
}

Problems with This Approach

Why it's bad:

// 1. TWO LOOKUPS on the same key
if (!map.has(key)) {  // <- lookup 1
  map.set(key, []);
}
map.get(key);         // <- lookup 2

// 2. TOO VERBOSE
// 3 lines for something that should be 1

// 3. ERROR PRONE
// Easy to forget to check first
const value = map.get(key);
value.push(item);  // TypeError if key doesn't exist!

// 4. NOT ATOMIC
// In theory, another operation could interfere
// between has() and set() (less relevant in JS single-thread)

The Solution: getOrInsert

New native method.

Syntax

How it works:

// getOrInsert(key, defaultValue)
// Returns existing value OR inserts and returns defaultValue

const map = new Map();

// If 'a' doesn't exist, inserts 0 and returns 0
const value = map.getOrInsert('a', 0);
console.log(value);  // 0
console.log(map.get('a'));  // 0

// If 'a' already exists, returns existing value
map.set('a', 42);
const existing = map.getOrInsert('a', 0);
console.log(existing);  // 42 (doesn't overwrite)

Practical Example: Counter

Counting occurrences:

// BEFORE (verbose):
const counts = new Map();
for (const word of words) {
  if (!counts.has(word)) {
    counts.set(word, 0);
  }
  counts.set(word, counts.get(word) + 1);
}

// AFTER (clean):
const counts = new Map();
for (const word of words) {
  const count = counts.getOrInsert(word, 0);
  counts.set(word, count + 1);
}

// Or even cleaner with mutable object:
const counts = new Map();
for (const word of words) {
  const counter = counts.getOrInsert(word, { count: 0 });
  counter.count++;
}

Practical Example: Grouping

Grouping by property:

// BEFORE:
function groupBy(items, keyFn) {
  const groups = new Map();
  for (const item of items) {
    const key = keyFn(item);
    if (!groups.has(key)) {
      groups.set(key, []);
    }
    groups.get(key).push(item);
  }
  return groups;
}

// AFTER:
function groupBy(items, keyFn) {
  const groups = new Map();
  for (const item of items) {
    const key = keyFn(item);
    groups.getOrInsert(key, []).push(item);
  }
  return groups;
}

// Usage:
const byCategory = groupBy(products, p => p.category);

getOrInsertComputed

For dynamic values.

Why It Exists

The problem with getOrInsert:

// getOrInsert always creates the default value
// even if it doesn't need it

class ExpensiveObject {
  constructor() {
    console.log('Creating expensive object...');
    // Heavy operation
  }
}

const cache = new Map();

// PROBLEM: ExpensiveObject is created ALWAYS
// even if 'key' already exists in map!
cache.getOrInsert('key', new ExpensiveObject());

// Each call creates a new object,
// even when the cache already has the value

Syntax

How it works:

// getOrInsertComputed(key, callbackFn)
// callbackFn is only called if key DOES NOT exist

const cache = new Map();

// Callback only executes if 'key' doesn't exist
const value = cache.getOrInsertComputed('key', () => {
  console.log('Computing value...');
  return new ExpensiveObject();
});

// Second call: callback DOES NOT execute
const cached = cache.getOrInsertComputed('key', () => {
  console.log('This will not appear');
  return new ExpensiveObject();
});

console.log(value === cached);  // true

Practical Example: Computation Cache

Memoization:

// Cache of computed results
const computationCache = new Map();

function computeExpensive(input) {
  return computationCache.getOrInsertComputed(input, () => {
    console.log(`Computing for: ${input}`);
    // Heavy operation
    let result = 0;
    for (let i = 0; i < 1000000; i++) {
      result += Math.sqrt(i * input);
    }
    return result;
  });
}

// First call: computes
computeExpensive(42);  // "Computing for: 42"

// Second call: cache hit
computeExpensive(42);  // (silent, returns cache)

// New input: computes again
computeExpensive(100);  // "Computing for: 100"

Practical Example: Factory Pattern

Lazy creation:

class ConnectionPool {
  #connections = new Map();

  getConnection(database) {
    return this.#connections.getOrInsertComputed(database, () => {
      console.log(`Creating connection for: ${database}`);
      return new DatabaseConnection(database);
    });
  }
}

const pool = new ConnectionPool();

// First call: creates connection
const conn1 = pool.getConnection('users');

// Same connection reused
const conn2 = pool.getConnection('users');
console.log(conn1 === conn2);  // true

// New connection for another database
const conn3 = pool.getConnection('products');
console.log(conn1 === conn3);  // false

WeakMap Also Gets It

Same methods.

WeakMap Support

Works the same:

const weakCache = new WeakMap();

class Component {
  constructor(id) {
    this.id = id;
  }
}

const component = new Component(1);

// getOrInsert on WeakMap
const metadata = weakCache.getOrInsert(component, {
  renderCount: 0,
  lastUpdate: null,
});

metadata.renderCount++;

// getOrInsertComputed on WeakMap
const computed = weakCache.getOrInsertComputed(component, () => ({
  expensiveData: calculateExpensiveData(component),
}));

Use Case: Object Metadata

Associating data with objects:

// Metadata without memory leaks
const objectMetadata = new WeakMap();

function trackObject(obj) {
  const meta = objectMetadata.getOrInsert(obj, {
    createdAt: Date.now(),
    accessCount: 0,
    history: [],
  });
  meta.accessCount++;
  meta.history.push(Date.now());
  return obj;
}

// When obj is garbage collected,
// metadata is also cleaned up

Comparing With Alternatives

Why use native.

Object.groupBy

Comparison:

// Object.groupBy (ES2024) - returns object
const grouped = Object.groupBy(items, item => item.category);
// { electronics: [...], clothing: [...] }

// Map.getOrInsert - returns Map
const grouped = new Map();
for (const item of items) {
  grouped.getOrInsert(item.category, []).push(item);
}

// When to use each:
// - Object.groupBy: final result is object, keys are strings
// - Map.getOrInsert: need Map, keys can be any type

Lodash _.get / _.set

Comparison:

// Lodash
import _ from 'lodash';

const obj = {};
_.set(obj, 'a.b.c', []);
_.get(obj, 'a.b.c', []).push(item);

// Native with Map
const map = new Map();
map.getOrInsert('key', []).push(item);

// Native advantages:
// - No dependency
// - Better performance
// - Correct types (TypeScript)

Simple Polyfill

For older browsers:

// Polyfill for Map.prototype.getOrInsert
if (!Map.prototype.getOrInsert) {
  Map.prototype.getOrInsert = function(key, defaultValue) {
    if (!this.has(key)) {
      this.set(key, defaultValue);
    }
    return this.get(key);
  };
}

// Polyfill for Map.prototype.getOrInsertComputed
if (!Map.prototype.getOrInsertComputed) {
  Map.prototype.getOrInsertComputed = function(key, callbackFn) {
    if (!this.has(key)) {
      this.set(key, callbackFn(key));
    }
    return this.get(key);
  };
}

Performance

Why native is better.

Benchmark

Comparing approaches:

Operation: 1M upserts

has() + set() + get() pattern:
├── Time: 245ms
├── Operations: 3 per upsert
└── Overhead: duplicate lookup

Native getOrInsert:
├── Time: 89ms
├── Operations: 1 per upsert
└── Overhead: minimal

Improvement: ~2.7x faster

Why It's Faster

Internal optimizations:

// Old pattern - 3 operations:
if (!map.has(key)) {    // 1. Hash + lookup
  map.set(key, value);  // 2. Hash + lookup + insertion
}
return map.get(key);    // 3. Hash + lookup

// getOrInsert - 1 operation:
map.getOrInsert(key, value);  // 1. Hash + lookup + conditional
// Internally optimized by the engine

Common Patterns

Refactoring existing code.

Frequency Counter

Before and after:

// BEFORE:
function countFrequency(items) {
  const freq = new Map();
  for (const item of items) {
    freq.set(item, (freq.get(item) || 0) + 1);
  }
  return freq;
}

// AFTER:
function countFrequency(items) {
  const freq = new Map();
  for (const item of items) {
    const counter = freq.getOrInsert(item, { n: 0 });
    counter.n++;
  }
  return freq;
}

// Or with primitive value:
function countFrequency(items) {
  const freq = new Map();
  for (const item of items) {
    const count = freq.getOrInsert(item, 0);
    freq.set(item, count + 1);
  }
  return freq;
}

Adjacency List (Graphs)

Before and after:

// BEFORE:
function buildGraph(edges) {
  const graph = new Map();
  for (const [from, to] of edges) {
    if (!graph.has(from)) graph.set(from, []);
    if (!graph.has(to)) graph.set(to, []);
    graph.get(from).push(to);
  }
  return graph;
}

// AFTER:
function buildGraph(edges) {
  const graph = new Map();
  for (const [from, to] of edges) {
    graph.getOrInsert(from, []).push(to);
    graph.getOrInsert(to, []);  // Ensures node exists
  }
  return graph;
}

Multi-Map

Mapping to multiple values:

// BEFORE:
class MultiMap {
  #map = new Map();

  add(key, value) {
    if (!this.#map.has(key)) {
      this.#map.set(key, new Set());
    }
    this.#map.get(key).add(value);
  }
}

// AFTER:
class MultiMap {
  #map = new Map();

  add(key, value) {
    this.#map.getOrInsert(key, new Set()).add(value);
  }
}

Support Timeline

When to use in production.

Current Status (January 2026)

Implementation:

Browser Status Version
Chrome Stable 145+
Edge Stable 145+
Firefox In development ~130
Safari In development ~27
Node.js Stable 22+
Deno Stable 1.40+
Bun Stable 1.1+

Recommendation

When to adopt:

Now (January 2026):
├── Node.js / Deno / Bun: use freely
├── Modern browser: Chrome/Edge ok
├── All browsers: use with polyfill
└── TypeScript: types already available

Q2 2026 (expected):
├── Firefox stable
├── Safari stable
└── Remove polyfill for most users

Q4 2026 (expected):
├── Baseline feature
└── Use without concern

Conclusion

getOrInsert and getOrInsertComputed are the type of addition that seems small but impacts real code daily. The "check if exists, insert if not, get value" pattern is so common that native support eliminates dozens of repetitive lines per project.

Performance also matters: avoiding duplicate lookups in Map can make a difference in intensive loops. And the semantics are clearer - the method name says exactly what it does.

For new projects on Node.js or modern browsers, start using it now. For projects that need broad compatibility, the 10-line polyfill works until all browsers implement it.

It's another step of JavaScript toward the ergonomics that other languages have had for years.

If you want to understand more about new ES2026 features, check out our article on Import Defer for another important addition to the version.

Let's go! 🦅

💻 Master JavaScript for Real

The knowledge you acquired in this article is just the beginning. Understanding Map and data structures is fundamental for efficient code.

Invest in Your Future

I've prepared complete material for you to master JavaScript:

Payment options:

  • 1x of $4.90 interest-free
  • or $4.90 cash

📖 See Full Content

Comments (0)

This article has no comments yet 😢. Be the first! 🚀🦅

Add comments