Five Small ES2026 APIs That Delete Utility Code You Have Been Writing for Years
Error.isError, Map.getOrInsert, Uint8Array base64 and hex, Math.sumPrecise and Iterator.concat — the unglamorous half of ES2026, and the helper functions each one retires.
Every ECMAScript edition has two halves. There is the half that gets the conference talks — this year that was Array.fromAsync, Promise.try and explicit resource management — and there is the half that quietly deletes forty lines from your shared utils file and never gets mentioned again.
ECMAScript 2026 was approved by Ecma International on 30 June 2026, and its second half is unusually good. Five additions, none of them syntax, all of them replacing a workaround that most codebases have written at least once: Error.isError, the getOrInsert family on Map and WeakMap, base64 and hex methods on Uint8Array, Math.sumPrecise, and Iterator.concat from the iterator sequencing proposal.
Here is what each one actually replaces, and where each one still has a sharp edge.
Error.isError, and why instanceof was never enough
Checking whether a value is an Error is one of those problems that looks solved until you hit a realm boundary. An error thrown inside an iframe, a worker, or a Node vm context fails an instanceof Error check in the parent realm, because it is an instance of a different Error constructor entirely.
The workaround everyone converged on was a Object.prototype.toString brand check, which is both ugly and wrong — it can be spoofed with Symbol.toStringTag:
// the old workaround
const looksLikeError = (v) =>
Object.prototype.toString.call(v) === '[object Error]'
// which this defeats
const liar = { [Symbol.toStringTag]: 'Error' }
looksLikeError(liar) // true. it is not an error.// ES2026
Error.isError(new Error('boom')) // true
Error.isError(new TypeError('boom')) // true
Error.isError(liar) // false
Error.isError({ message: 'boom' }) // falseError.isError inspects the internal slot that only genuine error objects have, so it is realm-independent and cannot be faked. If you maintain a library that normalises thrown values — a logger, an error reporter, a retry wrapper — this is the one to adopt first, because the old check was silently wrong in exactly the environments where errors matter most.
One thing it does not do: it is a check for error objects, not for thrown values. Code can throw a string, a number, or undefined, and Error.isError will correctly say false for all of them. Your normalisation layer still needs an else branch.
Map.getOrInsert, and the end of the double lookup
The grouping pattern is probably the single most-written snippet in JavaScript. You want a Map of arrays, and every insert needs the same three lines:
// before
for (const user of users) {
if (!byCity.has(user.city)) byCity.set(user.city, [])
byCity.get(user.city).push(user)
}That is two hash lookups on the miss path and two on the hit path, plus a conditional you have to read every time. ES2026 adds getOrInsert and getOrInsertComputed to both Map.prototype and WeakMap.prototype:
// after
for (const user of users) {
byCity.getOrInsert(user.city, []).push(user)
}There is a trap in that line, and it is worth being explicit about it. getOrInsert takes a value, and that value is evaluated on every iteration whether or not it gets used. For an empty array literal that is cheap and harmless. For anything expensive — a fresh database connection, a compiled regex, a parsed config — you want the lazy variant instead:
// eager: makeClient() runs on every call, even on a hit
clients.getOrInsert(region, makeClient(region))
// lazy: the callback runs only when the key is missing
clients.getOrInsertComputed(region, () => makeClient(region))The rule of thumb: literal defaults use getOrInsert, constructed defaults use getOrInsertComputed. Both return the stored value, so they chain cleanly, and both work on WeakMap, which is where the memoisation use case lives.
Uint8Array to and from base64, without the round trip through strings
Encoding binary data as base64 in JavaScript has been embarrassing for as long as JavaScript has had binary data. The browser route goes through btoa, which only accepts a string of code units below 256, so you first have to build a binary string one byte at a time:
// the old browser dance
const toBase64 = (bytes) => {
let binary = ''
for (const byte of bytes) binary += String.fromCharCode(byte)
return btoa(binary)
}That allocates a string roughly the size of your data, blows the call stack if you try to shortcut it with apply on a large array, and has no counterpart in the other direction that is any nicer. Node users reached for Buffer instead, which is not portable. ES2026 puts the methods where they belong:
const bytes = new Uint8Array([72, 101, 108, 108, 111])
bytes.toBase64() // 'SGVsbG8='
bytes.toHex() // '48656c6c6f'
Uint8Array.fromBase64('SGVsbG8=') // Uint8Array(5)
Uint8Array.fromHex('48656c6c6f') // Uint8Array(5)There is an options bag for the URL-safe alphabet, which is what you want for anything that travels in a query string or a JWT segment:
bytes.toBase64({ alphabet: 'base64url' })
Uint8Array.fromBase64(token, { alphabet: 'base64url' })This is the one in the list with the most uneven support history — it reached stage 4 in July 2025 and landed in the non-V8 engines well before V8, so check current support for your target runtimes rather than assuming. It is also the easiest to polyfill safely, since the semantics are fully specified and there is no syntax involved.
Math.sumPrecise, for when floating point embarrasses you
Everyone knows the 0.1 plus 0.2 example. What people underestimate is how fast the error compounds once you are summing a long array, and how order-dependent the result becomes:
const xs = [1e20, 0.1, -1e20]
xs.reduce((a, b) => a + b, 0) // 0
Math.sumPrecise(xs) // 0.1The reduce loses the 0.1 entirely, because adding it to 1e20 cannot be represented and the value is simply discarded before the subtraction brings the magnitude back down. Math.sumPrecise takes an iterable and computes the correctly-rounded sum, so the answer does not depend on the order of the input.
Where this matters in ordinary application code: summing a column of currency amounts, aggregating measurements of wildly different magnitudes, or anything where a user is going to compare your total against one computed elsewhere. Where it does not matter: adding up three numbers, or anything you were going to round to two decimal places anyway. It is slower than a naive loop by construction, so reach for it when correctness is the point.
Note that it takes an iterable rather than being a variadic function, so it is Math.sumPrecise(array), not Math.sumPrecise(...array) — which is a feature, since the spread version would hit the argument-count limit on large arrays.
Iterator.concat, the missing piece of iterator helpers
Iterator helpers shipped a set of lazy operations — map, filter, take, drop, flatMap — and most people noticed the gap immediately: there was no lazy way to run several iterators one after another. You could spread them all into an array, which defeats the laziness, or write a generator, which is three lines of ceremony for something that should be an expression.
// before
function* chain(...its) {
for (const it of its) yield* it
}
// after
Iterator.concat(recentLogs(), archivedLogs(), remoteLogs())
.filter((l) => l.level === 'error')
.take(20)
.toArray()The laziness is the entire point here. In that example, archivedLogs and remoteLogs are never touched if the first twenty errors all come from recentLogs — which is exactly the behaviour you want when the later sources are expensive. If you are new to the helper methods, the iterator helpers guide covers the rest of the surface.
What to adopt now, and what to wait on
These five are not equally ready, and treating them as one batch is how you end up shipping a runtime error to an older browser. A rough ordering:
- Error.isError — adopt immediately in library code, behind a tiny fallback if you support old targets. The correctness win over instanceof is real.
- getOrInsert and getOrInsertComputed — trivially polyfillable, and the readability gain is immediate. Just keep the eager-versus-lazy distinction straight.
- Uint8Array base64 and hex — high value, most uneven support. Feature-detect, or pull in a polyfill that matches the spec options bag.
- Math.sumPrecise — adopt where accuracy is load-bearing, ignore everywhere else. It is not a general replacement for reduce.
- Iterator.concat — the newest of the group, so check your runtime baseline before leaning on it.
None of these change how JavaScript reads, which is why they do not make the highlight reels. They just remove five small opportunities to be subtly wrong, and a codebase is mostly made of those.
If you want the louder half of the same release, the write-up on Array.fromAsync, Promise.try and RegExp.escape covers it, and explicit resource management with the using keyword is the one that actually changes how you write cleanup code.