Sitemap

Node.js 26 LTS: Map Upsert, Iterator.concat and Other Small Features That Delete Utility Code

19 min read2 days ago

--

Node.js 26 became the new LTS line this week. The headline features are big, but the ones that will change your day-to-day code are small: two new Map methods and one new Iterator function that each replace a helper you have written many times.

Press enter or click to view image in full size

Every Node.js codebase I have worked on has a utils folder. Somewhere inside it there is a groupBy helper, a getOrCreate helper, a chain or concatIterables generator, and a date helper that adds a month without breaking on January 31. Nobody is proud of these files. They exist because the language did not give us a clean way to do very ordinary things.

Node.js 26 LTS deletes a good chunk of that folder. In this post we will walk through:

  1. Map upsert — Map.prototype.getOrInsert() and getOrInsertComputed() (and the same two on WeakMap).
  2. Iterator.concat() — join any number of iterables into one lazy stream.
  3. Other small wins — Temporal switched on by default, and built-in Base64/Hex on Uint8Array.
  4. Upgrade notes — the few removals that can break your app when you move from Node.js 24.

Every section starts from zero. If you know what a Map is and have written a for...of loop, you have enough background. Where an idea needs theory — why an argument is evaluated early, what "lazy" actually means — we will slow down and draw it.

First, what does “Node.js 26 LTS” actually mean?

On October 28, 2026 the Node.js project shipped v26.13.0 under the codename “Lithium” and moved the 26.x line into Long Term Support. That release contains no new code compared with 26.12.1 — it only flips the metadata. The features themselves arrived months earlier, when 26.0.0 shipped on May 5, 2026 as a “Current” release.

If the release labels are new to you, here is the short version:

  • Current — the newest major. It gets new features quickly and can still change. Good for trying things, not for production.
  • Active LTS — the same major, now frozen for features and focused on bug and security fixes. This is the line most teams should run in production.
  • Maintenance LTS — critical fixes only, until end of life. Node.js 26 is supported until the end of April 2029.
Press enter or click to view image in full size

There is one more reason this release matters: 26 is the last major on the old “two releases a year, only even numbers become LTS” schedule. Starting with Node.js 27 (April 2027), the project moves to one major release per year, and every major becomes LTS. So if you skipped Node.js 24, 26 is the natural jump.

Which engine features did we get?

Node.js 26 ships the V8 14.6 JavaScript engine (the one inside Chromium 146). The release notes call out two new language features that come with it — and those are the stars of this post:

  • Upsert: Map.prototype.getOrInsert(), Map.prototype.getOrInsertComputed() and the same pair on WeakMap.
  • Iterator sequencing: Iterator.concat().

Both are part of the ECMAScript 2026 standard, which means they are not Node-only tricks: the same code runs in current browsers too.

How to follow along

Install Node.js 26 with your version manager and confirm you are on the LTS build:

nvm install 26
nvm use 26
node -p "process.version + ' ' + process.release.lts"
# v26.13.0 Lithium

Then do a quick feature check. Each line should print function:

// check.mjs
console.log(typeof Map.prototype.getOrInsert);
console.log(typeof Map.prototype.getOrInsertComputed);
console.log(typeof Iterator.concat);

If you see undefined, you are still on an older Node.js. Every example below is a plain .mjs file you can run with node file.mjs.

Part 1 — Map upsert: “get it, or create it” in one call

The word upsert is borrowed from databases: update if the row exists, insert if it doesn’t. In JavaScript the idea is simpler still: give me the value for this key — and if there isn’t one yet, put a default in first.

A 30-second refresher on Map

A Map stores key → value pairs. Unlike a plain object, its keys can be anything (strings, numbers, objects), it remembers insertion order, and it has a clean API:

const prices = new Map();
prices.set('apple', 1.2); // add or overwrite
prices.get('apple'); // 1.2
prices.get('mango'); // undefined (missing key)
prices.has('mango'); // false
prices.size; // 1

Notice that get() on a missing key returns undefined. That one fact is the root of the pattern we are about to delete.

The problem: the has / set / get dance

Say we want to count how often each word appears. Before Node.js 26 you would write something like this:

const words = ['node', 'map', 'node', 'iterator', 'map', 'node'];
const counts = new Map();

for (const w of words) {
if (!counts.has(w)) { // 1. is the key there?
counts.set(w, 0); // 2. if not, insert a starting value
}
counts.set(w, counts.get(w) + 1); // 3. read it back and update
}
console.log(counts);
// Map(3) { 'node' => 3, 'map' => 2, 'iterator' => 1 }

It works, but look at what it costs:

  • The key is written three or four times. Every repetition is a chance to typo w as word in one place and not the others.
  • The map is searched up to three times (has, set, get) for one logical operation.
  • The intent is buried. The reader has to decode an if with a negation to understand "start at zero".

Grouping has exactly the same shape — check, create an empty array, read it back, push:

if (!byCustomer.has(order.customer)) {
byCustomer.set(order.customer, []);
}
byCustomer.get(order.customer).push(order.id);

Because this is tedious, people reach for a “shortcut” that contains a classic bug:

const groups = new Map();
(groups.get('asha') || []).push(1); // looks fine...
console.log(groups.size); // 0 <-- the array was never stored!

The || [] creates a brand-new array when the key is missing, you push into it, and then it is thrown away because nobody called set(). The bug is silent: no error, just missing data.

Press enter or click to view image in full size

Meet getOrInsert(key, defaultValue)

getOrInsert does all three steps in one call:

  • If key is already in the map, it returns the existing value and changes nothing.
  • If key is missing, it stores defaultValue under key and returns it.

Either way, you always get a real value back — never undefined for a missing key. Here is the word counter again:

const counts = new Map();

for (const w of words) {
counts.set(w, counts.getOrInsert(w, 0) + 1);
}
console.log(counts);
// Map(3) { 'node' => 3, 'map' => 2, 'iterator' => 1 }

Read the loop body from the inside out: “get the count for w (starting at 0 if it's new), add one, store it." One line, the key written twice instead of four times, and no if.

Why is there still a set()? Numbers are values, not containers. getOrInsert hands you the number 0; adding 1 to it creates a new number that the map knows nothing about. So for counters you still write the result back. For containers (arrays, Sets, objects) you don't — you mutate the container the map already holds, as the next example shows.

Meet getOrInsertComputed(key, callback)

The second method takes a function instead of a ready-made value. The map only calls that function when the key is missing, and it passes the key in as the argument.

Press enter or click to view image in full size

Grouping orders by customer becomes:

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

const byCustomer = new Map();
for (const order of orders) {
byCustomer.getOrInsertComputed(order.customer, () => []).push(order.id);
}
console.log(byCustomer);
// Map(2) { 'asha' => [ 1, 3 ], 'ben' => [ 2 ] }

Step by step, for the first order (asha): the key is missing, so the map calls () => [], stores the new empty array under 'asha', and returns that same array — which we immediately .push(1) into. For the third order (asha again) the key exists, the callback is skipped, and we get the array that already holds [1]. Because it is the very same array object, pushing into it updates the map. No set() needed.

The callback receives the key, which is handy when the default depends on it:

const squares = new Map();
squares.getOrInsertComputed(7, (n) => n * n); // 49 — computed and stored
squares.get(7); // 49

The theory: why are there two methods?

This is the most important idea in the whole feature, so let’s slow down.

In JavaScript, every argument is evaluated before the function you’re calling starts running. When you write f(a(), b()), the engine first runs a(), then b(), and only then calls f with the two results. f has no say in the matter.

Apply that to getOrInsert:

cache.getOrInsert('user:1', loadUser());

loadUser() runs first, every time — even if 'user:1' is already cached. getOrInsert then notices the key exists, returns the old value, and the freshly built object is thrown away. If loadUser() is expensive (a database query, a big allocation, a log line), you pay for it on every call.

getOrInsertComputed avoids this because you pass the function itself, not its result. A function is just a recipe; nothing happens until someone calls it — and the map only calls it on a miss. This idea is called lazy evaluation: do the work only when the result is actually needed.

You can prove it with a counter:

let calls = 0;
const makeExpensive = () => { calls++; return { createdAt: 'now' }; };
const cache = new Map([['user:1', { createdAt: 'earlier' }]]);

cache.getOrInsert('user:1', makeExpensive()); // eager
console.log(calls); // 1 <- ran even though the key existed

cache.getOrInsertComputed('user:1', makeExpensive); // lazy, key exists
console.log(calls); // 1 <- callback skipped

cache.getOrInsertComputed('user:2', makeExpensive); // lazy, key missing
console.log(calls); // 2 <- ran only on the miss
Press enter or click to view image in full size

Rule of thumb:

  • Use getOrInsert for cheap constants: 0, '', false, null.
  • Use getOrInsertComputed for anything you build: [], {}, new Set(), new Map(), a calculation, a network call.

There is a second, sneakier reason for that rule. Look at what happens if you try to “optimise” by creating the empty array once:

const EMPTY = [];
const groups = new Map();
groups.getOrInsert('asha', EMPTY).push(1);
groups.getOrInsert('ben', EMPTY).push(2);

console.log(groups.get('asha')); // [ 1, 2 ] <- ben's order leaked in
console.log(groups.get('asha') === groups.get('ben')); // true

Both keys now point at the same array in memory, so every push lands in every group. With getOrInsertComputed(key, () => []), the arrow function builds a fresh array on each miss, so every key gets its own.

Real-world recipes

1. Memoization (caching function results). The callback receives the key, so a pure function can be plugged in directly:

const memo = new Map();
const fib = (n) => memo.getOrInsertComputed(n, (k) => (k < 2 ? k : fib(k - 1) + fib(k - 2)));

console.log(fib(40)); // 102334155 - instant, each n computed once

2. Graphs as adjacency lists. Each node maps to a Set of neighbours. The second call makes sure nodes that only appear as targets still exist:

const edges = [['A', 'B'], ['A', 'C'], ['B', 'C'], ['C', 'A']];
const graph = new Map();

for (const [from, to] of edges) {
graph.getOrInsertComputed(from, () => new Set()).add(to);
graph.getOrInsertComputed(to, () => new Set());
}
// Map(3) { 'A' => Set { 'B', 'C' }, 'B' => Set { 'C' }, 'C' => Set { 'A' } }

3. Nested maps without nested ifs. Because each call returns the value, you can chain them — tenant → user → session ids:

const index = new Map();
for (const s of sessions) {
index
.getOrInsertComputed(s.tenant, () => new Map())
.getOrInsertComputed(s.user, () => [])
.push(s.id);
}
// Map { 'acme' => Map { 'u1' => ['s1','s2'], 'u2' => ['s3'] }, 'globex' => Map { 'u9' => ['s4'] } }

The old version of this needed two has checks, two sets and two gets.

4. De-duplicating in-flight requests. If ten callers ask for the same URL at once, only one network request should go out. Store the promise:

const inFlight = new Map();

function fetchOnce(url) {
return inFlight.getOrInsertComputed(url, () =>
fetch(url).finally(() => inFlight.delete(url)) // forget it once settled
);
}
await Promise.all([fetchOnce('/a'), fetchOnce('/a'), fetchOnce('/b')]);
// only 2 requests are made: one for /a, one for /b

The laziness matters here: with getOrInsert(url, fetch(url)) every caller would fire its own request before the map could say "already in flight".

What about Map.groupBy?

If you already have the whole array in hand and just want it grouped once, Map.groupBy(orders, (o) => o.customer) (available since Node.js 21) is even shorter. Reach for upsert when data arrives over time — events, stream chunks, messages — or when you need something other than an array per key (a counter, a Set, a nested Map, a promise).

The WeakMap versions

WeakMap gets the same two methods. A WeakMap only accepts objects (and non-registered symbols) as keys, and it holds them weakly: when nothing else in your program references the key object, the garbage collector may remove it and its value. That makes it perfect for attaching metadata to objects you don't own, without causing memory leaks:

const meta = new WeakMap();
const getMeta = (obj) => meta.getOrInsertComputed(obj, () => ({ hits: 0 }));

const req = { url: '/health' };
getMeta(req).hits++;
getMeta(req).hits++;
console.log(getMeta(req).hits); // 2
// when `req` is no longer referenced anywhere, its metadata can be collected too

Passing a string or number as the key throws a TypeError, exactly like WeakMap.prototype.set() does.

Small print

  • getOrInsertComputed throws a TypeError if the second argument isn't a function — map.getOrInsertComputed('k', 42) fails fast instead of storing 42.
  • Keys are compared the same way as everywhere else in Map: primitives by value, objects by reference. Two different { id: 1 } objects are two different keys.
  • Keep callbacks simple. Don’t make them insert the same key they’re computing; the value the callback returns is what ends up stored.
  • Need this on an older runtime or browser? core-js and the es-shims packages ship polyfills for both methods.

Part 2 — Iterator.concat(): one lazy stream from many sources

Iterator.concat(a, b, c) gives you a single iterator that yields everything from a, then everything from b, then everything from c — without copying anything into a new array first. To see why that matters, we need two words that confuse a lot of people: iterable and iterator.

Iterables and iterators, in plain words

Think of a music playlist.

  • The playlist is the iterable: a thing that can be played through. Arrays, strings, Sets, Maps and generator objects are all iterables. Technically, an iterable is any object with a [Symbol.iterator]() method.
  • The “next track” button is the iterator: a cursor that remembers where you are. Each press — each call to .next() — gives you one item, as { value, done }.
const it = [10, 20][Symbol.iterator](); // ask the array for a cursor

it.next(); // { value: 10, done: false }
it.next(); // { value: 20, done: false }
it.next(); // { value: undefined, done: true } <- finished

You rarely call .next() by hand. for...of, the spread operator [...x], Array.from(x) and destructuring all do it for you behind the scenes.

Get Vasanthan K’s stories in your inbox

Join Medium for free to get updates from this writer.

Since Node.js 22, iterators also have helper methods that work like the array methods you know — .map(), .filter(), .take(), .drop(), .flatMap(), .find(), .some(), .toArray() and friends — but they process one item at a time instead of building whole arrays. Iterator.concat() is the missing piece that lets you start such a chain from several sources at once.

The problem: joining several sources

Suppose a web framework has built-in routes in an array, plugin routes in a Set, and a generator that produces fallback routes. You want to loop over all of them. Before Node.js 26 you had two choices.

Option A — spread everything into one array:

const all = [...builtIn, ...pluginRoutes, ...fallbacks()];

Short, but eager: it reads every value from every source and allocates a brand-new array to hold them, even if you only need the first match. It cannot work at all with a source that never ends.

Option B — write your own generator (again):

function* chain(...iterables) {
for (const it of iterables) yield* it;
}
const all = chain(builtIn, pluginRoutes, fallbacks());

This one is lazy, but it’s another helper to copy between projects, test and name — and it doesn’t validate its inputs until you start looping.

Meet Iterator.concat()

function* lows() { yield 1; yield 2; yield 3; }
const highs = new Set([7, 8, 9]);

const digits = Iterator.concat(lows(), [4, 5, 6], highs);
console.log([...digits]); // [1, 2, 3, 4, 5, 6, 7, 8, 9]

Three different kinds of source — a generator, an array and a Set — become one iterator. The values come out in exactly the order the sources were passed. The result is a real Iterator, so every helper method is available on it immediately.

Press enter or click to view image in full size

The theory: what “lazy” really means here

When you call Iterator.concat(...), no values are read yet. You just get back a cursor. Work only happens when somebody calls .next() on it, and then:

  1. The request is forwarded to the currently active source (at the start, the first one).
  2. If that source has a value, it is handed straight back to the caller.
  3. If that source is finished, Iterator.concat moves on and opens the next source — not before.
  4. When the last source is finished, it reports { done: true }.

You can watch this happen by logging from inside the sources:

const order = [];
function* source(name, values) {
for (const v of values) { order.push(`${name} gives ${v}`); yield v; }
}

const all = Iterator.concat(source('A', [1, 2]), source('B', [3, 4]));
order.push('nothing pulled yet');
const first = all.next().value;
order.push(`consumer got ${first}`);
console.log(order);
// [ 'nothing pulled yet', 'A gives 1', 'consumer got 1' ]

Notice that 'nothing pulled yet' is logged before source A produced anything, and source B hasn't run at all. That is laziness in one picture: the consumer drives the work, one value at a time.

Press enter or click to view image in full size

Combining it with iterator helpers

This is where Iterator.concat() really earns its place. Build one stream, then shape it:

const builtIn  = ['/health', '/login', '/admin/users'];
const plugins = new Set(['/blog', '/admin/settings']);
const fallback = ['/404'];

const publicRoutes = Iterator.concat(builtIn, plugins, fallback)
.filter((r) => !r.startsWith('/admin'))
.map((r) => r.toUpperCase())
.toArray();
console.log(publicRoutes); // [ '/HEALTH', '/LOGIN', '/BLOG', '/404' ]

No intermediate arrays are created between concat, filter and map. Each route travels through the whole chain on its own; only the survivors land in the final array built by .toArray().

And because nothing is read ahead, stopping early really stops the work:

let pulledFromSecond = 0;
function* second() {
for (let i = 0; i < 1_000_000; i++) { pulledFromSecond++; yield i; }
}

const firstThree = Iterator.concat(['a', 'b'], second()).take(3).toArray();
console.log(firstThree); // [ 'a', 'b', 0 ]
console.log(pulledFromSecond); // 1 <- not 1,000,000

The same property lets you mix in infinite sources safely, as long as something downstream limits how much you take:

function* naturals() { let n = 1; while (true) yield n++; }

Iterator.concat(['start'], naturals()).take(5).toArray();
// [ 'start', 1, 2, 3, 4 ]

More everyday uses

Merging Maps (later sources win). A Map iterates as [key, value] pairs, and the Map constructor accepts pairs, so concatenating maps is merging them:

const defaults = new Map([['port', 3000], ['host', 'localhost']]);
const fromEnv = new Map([['port', 8080]]);

const config = new Map(Iterator.concat(defaults, fromEnv));
// Map(2) { 'port' => 8080, 'host' => 'localhost' }

Reading paged data as one stream. If each page is a generator, the caller never has to think about page boundaries:

function* page(n) { for (let i = 1; i <= 3; i++) yield `p${n}-item${i}`; }

Iterator.concat(page(1), page(2), page(3)).take(5).toArray();
// [ 'p1-item1', 'p1-item2', 'p1-item3', 'p2-item1', 'p2-item2' ]

Page 3 is never touched.

Rules and gotchas

1. An iterator can only be read once. It’s a cursor, not a container. Once it reaches the end, it stays there:

const once = Iterator.concat([1, 2]);
console.log([...once]); // [ 1, 2 ]
console.log([...once]); // [] <- already exhausted

If you need to loop twice, call Iterator.concat(...) twice (or .toArray() once and reuse the array).

2. Every argument must be an iterable object — checked immediately. The arguments are validated the moment you call Iterator.concat, not later when you start looping. Two cases surprise people:

Iterator.concat('ab');   // TypeError — a string is a primitive, not an object

const bare = { // has next() but no [Symbol.iterator]()
i: 0,
next() { return this.i < 2 ? { value: this.i++, done: false } : { value: undefined, done: true }; },
};
Iterator.concat(bare); // TypeError - an iterator that isn't iterable

The fix for both is Iterator.from(), which wraps anything iterable-ish into a proper Iterator:

[...Iterator.concat(Iterator.from('ab'), [1])]; // [ 'a', 'b', 1 ]
[...Iterator.concat(Iterator.from(bare))]; // [ 0, 1 ]

Why so strict? Iterator.concat takes ownership of the iterators it opens and promises to close them. With a bare iterator it would be unclear who is responsible for closing it, so the standard simply refuses.

3. Stopping early closes the active source for you. If you break out of a loop (or .take() stops), the source currently being read gets its cleanup code run — the finally block of a generator, for example. Sources that were never reached are never opened, so there is nothing to clean up:

function* resource(name) {
try { yield `${name}-1`; yield `${name}-2`; }
finally { console.log(`${name} closed`); }
}

for (const v of Iterator.concat(resource('fileA'), resource('fileB'))) {
console.log('read', v);
if (v === 'fileA-1') break;
}
// read fileA-1
// fileA closed <- fileB was never opened

This is the behaviour you want for file handles, database cursors and anything else that must be released.

4. It is synchronous only. Async generators and streams are async iterables and will throw a TypeError. For those, a tiny async function* with for await and yield* is still the tool.

5. Huge or unknown numbers of sources? Iterator.concat(...arrayOfIterables) spreads every source into arguments. When you have thousands of sources — or an endless list of them — use .flatMap() on an iterator of iterables instead: Iterator.from(listOfSources).flatMap((s) => s).

Part 3 — Other small features that delete utility code

Map upsert and Iterator.concat() are the new language features in V8 14.6. Two more changes make Node.js 26 LTS a good moment to clean out the utils folder — especially if you're jumping straight from Node.js 24.

Temporal is on by default

Temporal is the modern replacement for JavaScript's Date object, and Node.js 26 is the first release where it is enabled by default — no flag, no polyfill. It deserves its own article, so here are only the parts that replace common helpers.

The problem with Date in one example — adding a month to January 31:

const d = new Date(2026, 0, 31); // Jan 31, 2026
d.setMonth(d.getMonth() + 1);
console.log(d.toDateString()); // 'Tue Mar 03 2026' <- overflowed into March

Date happily creates "February 31" and rolls it forward three days. Most teams have a helper (or a library) just to avoid this. With Temporal:

const jan31 = Temporal.PlainDate.from('2026-01-31');

jan31.add({ months: 1 }).toString(); // '2026-02-28' <- clamped to the last valid day
jan31.toString(); // '2026-01-31' <- the original is untouched

Two ideas make Temporal easier to reason about than Date:

  • Separate types for separate questions. A PlainDate is a calendar date with no time and no time zone (a birthday, a due date). A ZonedDateTime is an exact moment in a specific zone (a meeting). You pick the type that matches your data, so a birthday can never shift by a day because a server runs in UTC.
  • Everything is immutable. Methods like .add() return a new object instead of changing the old one, so you can't accidentally modify a date someone else is holding.

A few more one-liners that used to need helpers:

// Date arithmetic
Temporal.PlainDate.from('2026-10-30').add({ days: 45 }).toString();
// '2026-12-14'

// "How long until...?" as a human-readable duration
Temporal.PlainDate.from('2026-10-28')
.until('2029-04-30', { largestUnit: 'years' })
.toString();
// 'P2Y6M2D' -> 2 years, 6 months, 2 days

// Time-zone conversion
Temporal.ZonedDateTime.from('2026-11-02T09:00[America/New_York]')
.withTimeZone('Asia/Kolkata')
.toString();
// '2026-11-02T19:30:00+05:30[Asia/Kolkata]'

// Today's date in the system time zone
Temporal.Now.plainDateISO();

You don’t have to rewrite every Date in your codebase. A good first step is to use Temporal in new code that does calendar maths or crosses time zones — exactly the places where date libraries and hand-written helpers live today.

Base64 and Hex built into Uint8Array

These methods landed in Node.js 25, so Node.js 26 is the first LTS that has them. They let any Uint8Array convert to and from Base64 and hex strings directly:

const bytes = new TextEncoder().encode('Hello, Node 26!');

bytes.toBase64(); // 'SGVsbG8sIE5vZGUgMjYh'
bytes.toHex(); // '48656c6c6f2c204e6f646520323621'

const back = Uint8Array.fromBase64('SGVsbG8sIE5vZGUgMjYh');
new TextDecoder().decode(back); // 'Hello, Node 26!'

Uint8Array.fromHex('cafe'); // Uint8Array(2) [ 202, 254 ]

URL-safe Base64 (for tokens, JWT segments and file names) is an option, not another helper:

const raw = new TextEncoder().encode('Hi?>');

raw.toBase64(); // 'SGk/Pg=='
raw.toBase64({ alphabet: 'base64url', omitPadding: true }); // 'SGk_Pg'

In pure Node.js code, Buffer.from(bytes).toString('base64') still works fine. The win is portability: the same lines run unchanged in current browsers, where these methods are part of the standard too. If you share code between a server and a frontend, you can delete the btoa(String.fromCharCode(...bytes)) dance and the per-platform if (typeof Buffer !== 'undefined') branches.

Part 4 — Before you upgrade: what can break

Small features are the fun part, but an LTS jump is also when old code stops working. These are the changes from the Node.js 26.0.0 release notes most likely to affect an app coming from Node.js 24:

Press enter or click to view image in full size

One more operational note from the LTS announcement: since Node.js 25, the official Linux binaries are linked against libatomic, which some slim container images don't include. If node fails to start in Docker after the upgrade, install that package first.

A safe rollout path: run your test suite on Node.js 26 in CI alongside 24, fix what fails, then switch production. Once you’re on 26, search the codebase for the old patterns (.has( followed by .set(, function* chain, yield* helpers, btoa() and replace them as you touch each file.

Cheat sheet: what to delete from your utils folder

Press enter or click to view image in full size

Wrapping up

None of these features will make a conference keynote. That’s the point. They remove small, repeated pieces of code that every team writes slightly differently — and every different version is a place for a bug to hide.

If you remember only three things:

  1. getOrInsertComputed(key, () => default) is the safe default for "get it, or create it". Use getOrInsert(key, value) only for cheap constants like 0, because its argument is always evaluated.
  2. Iterator.concat(a, b, c) joins sources lazily. Nothing is read until you ask, stopping early really stops, and cleanup runs for you. Wrap strings and bare iterators in Iterator.from() first.
  3. Node.js 26 is the LTS to move to. It’s supported until April 2029 and is the last release on the old schedule. Check the removals table above, then start deleting helpers.

Sources

--

--

Vasanthan K
Vasanthan K

Senior Full Stack Engineer | Scalable Enterprise Systems | Node.js • GraphQL • React/Vue • Mongodb | Azure Cloud | 10+ Year | Gen AI enthusiast