ECMAScript 2026 introduces improvements for Maps, Iterators, errors, bytes, numeric operations, asynchronous sources, and JSON without precision loss. Several already work in current runtimes, but support is not yet consistent.
The 17th edition of ECMA-262 does not introduce syntax that completely changes how we write JavaScript. Instead, it standardizes seven small solutions to problems we have spent years solving with helpers, libraries, or repetitive code.
ECMAScript 2026 is the 17th edition of the language standard. Its official additions are:
MapandWeakMapwithgetOrInsert.- Access to the original text in
JSON.parseandJSON.rawJSON. Iterator.concat.- Native Base64 and hexadecimal support in
Uint8Array. Math.sumPrecise.Error.isError.Array.fromAsync.
In this article, we will look at the problem each API solves, how to use it, and what support it actually has.
Before we begin: ES2026 is not the same as “everything new in JavaScript”
Some lists published online mix features from different editions.
These APIs belong to ECMAScript 2025, not 2026:
RegExp.escape.Promise.try.- Iterator Helpers.
- New
Setmethods. - Import Attributes and JSON Modules.
The following are not part of ES2026 either:
- Temporal.
- Explicit Resource Management and the
usingsyntax.
Both proposals reached Stage 4 after the edition cutoff and are planned for ECMAScript 2027.
1. Map.getOrInsert: get a value or create it
Grouping items in a Map usually requires two lookups:
const groups = new Map();
for (const item of items) {
if (!groups.has(item.category)) {
groups.set(item.category, []);
}
groups.get(item.category).push(item);
}ECMAScript 2026 adds:
Map.prototype.getOrInsert
Map.prototype.getOrInsertComputed
WeakMap.prototype.getOrInsert
WeakMap.prototype.getOrInsertComputedThe previous example becomes:
const groups = new Map();
for (const item of items) {
groups
.getOrInsert(item.category, [])
.push(item);
}If the key exists, it returns the current value. If it does not, it inserts the default value and returns it.
Compute the value only when needed
There is no reason to construct an expensive object if the key already exists:
const cache = new Map();
const profile = cache.getOrInsertComputed(
userId,
key => loadProfile(key)
);The getOrInsertComputed callback runs only when the key is not yet in the Map.
It also receives the key as an argument:
const groups = new Map();
const frontend = groups.getOrInsertComputed(
'frontend',
key => [key]
);
console.log(frontend);
// ['frontend']This API is similar to Java's computeIfAbsent, C#'s GetOrAdd, and Python's setdefault.
2. Array.fromAsync: convert an asynchronous source into an array
Until now, converting an async iterable required manually accumulating its values:
const values = [];
for await (const value of asyncSource) {
values.push(value);
}With ES2026:
const values = await Array.fromAsync(asyncSource);It also accepts a mapping function:
async function* pages() {
yield 1;
yield Promise.resolve(2);
}
const values = await Array.fromAsync(
pages(),
value => value * 10
);
console.log(values);
// [10, 20]It is not the same as Promise.all
Array.fromAsync consumes the source progressively and awaits its values sequentially, following for await behavior.
const sequential = await Array.fromAsync(promises);Promise.all first iterates over the entire source and awaits the results in parallel:
const parallel = await Promise.all(
Array.from(promises)
);The right option depends on whether you need lazy iteration, memory control, or parallelism.
3. Iterator.concat: combine iterables without creating intermediate arrays
To sequence multiple iterables, we used to write a generator:
function* concat(...iterables) {
for (const iterable of iterables) {
yield* iterable;
}
}ECMAScript 2026 adds Iterator.concat:
const values = Iterator.concat(
[1, 2],
new Set([3, 4]),
[5]
);
console.log([...values]);
// [1, 2, 3, 4, 5]The operation is lazy. Calling the method does not create an array containing all the elements.
This is useful for:
- Combining paginated results.
- Chaining multiple sources.
- Adding prefixes or suffixes to an Iterator.
- Processing large sequences without fully materializing them.
const records = Iterator.concat(
cachedRecords,
databaseRecords,
generatedRecords
);
for (const record of records) {
process(record);
}4. Base64 and hexadecimal directly in Uint8Array
JavaScript had atob and btoa, but those APIs work with binary strings, not byte arrays.
ES2026 adds:
Uint8Array.fromBase64()
Uint8Array.fromHex()
Uint8Array.prototype.toBase64()
Uint8Array.prototype.toHex()
Uint8Array.prototype.setFromBase64()
Uint8Array.prototype.setFromHex()Encode bytes
const bytes = new TextEncoder().encode('Hello');
console.log(bytes.toBase64());
// SGVsbG8=
console.log(bytes.toHex());
// 48656c6c6fDecode Base64
const bytes = Uint8Array.fromBase64('SGVsbG8=');
const text = new TextDecoder().decode(bytes);
console.log(text);
// HelloURL-safe Base64
const token = bytes.toBase64({
alphabet: 'base64url',
omitPadding: true,
});This variant is common in JWTs, URLs, and tokens.
Write into an existing buffer
setFromBase64 avoids creating another Uint8Array:
const target = new Uint8Array(8);
const result = target.setFromBase64('Zm9vYmFy');
console.log(result);
// { read: 8, written: 6 }5. Error.isError: detect genuine errors
The usual check is:
value instanceof ErrorThe problem appears when the error comes from another realm, such as an iframe, a worker, or a context created with node:vm.
const iframe = document.createElement('iframe');
document.body.append(iframe);
const externalError =
new iframe.contentWindow.Error('boom');
console.log(externalError instanceof Error);
// falseES2026 adds a check equivalent to Array.isArray:
console.log(Error.isError(externalError));
// trueIt also avoids relying on:
Object.prototype.toString.call(value)That technique can be fooled through Symbol.toStringTag.
Error.isError is useful in:
- Centralized logging systems.
- Exception serialization.
- Applications with iframes or workers.
- Code that may receive any value through
throw.
try {
await execute();
} catch (value) {
if (Error.isError(value)) {
logger.error(value.message, value.stack);
} else {
logger.error('Unknown thrown value', value);
}
}6. Math.sumPrecise: add numbers with less accumulated error
Sequential floating-point addition can lose small values:
const values = [1e20, 0.1, -1e20];
const total = values.reduce(
(sum, value) => sum + value,
0
);
console.log(total);
// 0The mathematical result is 0.1, but the value is lost because of the order of operations.
ECMAScript 2026 adds:
const total = Math.sumPrecise([
1e20,
0.1,
-1e20,
]);
console.log(total);
// 0.1The method takes an iterable, not separate arguments:
Math.sumPrecise(values);This avoids the stack limits that a variadic API would encounter with thousands of elements.
What it does not solve
Math.sumPrecise still works with number values.
It does not turn JavaScript into an exact decimal system and should not be used as a replacement for a decimal library in financial calculations.
It does not accept BigInt either.
Its goal is to return the correctly rounded sum we would obtain by adding with arbitrary precision and converting the result back to floating point.
7. JSON without precision loss
An API may return a perfectly valid integer that does not fit within JavaScript's safe integer range:
{
"user_id": 9223372036854775807
}The JSON preserves every digit, but JSON.parse converts the value to a number:
const payload =
'{"user_id":9223372036854775807}';
const result = JSON.parse(payload);
console.log(result.user_id);
// 9223372036854776000Converting that result to BigInt does not recover the lost information.
Access the original text
The JSON.parse reviver now receives a third argument:
const result = JSON.parse(
payload,
(key, value, context) => {
if (key === 'user_id') {
return BigInt(context.source);
}
return value;
}
);
console.log(result.user_id);
// 9223372036854775807ncontext.source contains the exact JSON fragment that produced the primitive value.
A reusable parser
export function parseLosslessJson(text) {
return JSON.parse(
text,
(_key, value, context) => {
const isUnsafeInteger =
typeof value === 'number' &&
!Number.isSafeInteger(value) &&
/^-?\d+$/.test(context.source);
return isUnsafeInteger
? BigInt(context.source)
: value;
}
);
}Safe integers and decimals remain number values:
const result = parseLosslessJson(
'{"user_id":9223372036854775807,"count":42,"ratio":0.125}'
);
console.log(result);
// {
// user_id: 9223372036854775807n,
// count: 42,
// ratio: 0.125
// }Serialize with JSON.rawJSON
JSON.stringify does not accept BigInt directly:
JSON.stringify({
user_id: 9223372036854775807n
});
// TypeErrorJSON.rawJSON lets you insert validated JSON text:
const encoded = JSON.stringify({
user_id: JSON.rawJSON(
'9223372036854775807'
)
});
console.log(encoded);
// {"user_id":9223372036854775807}A generic serializer:
export function stringifyLosslessJson(value) {
return JSON.stringify(
value,
(_key, current) =>
typeof current === 'bigint'
? JSON.rawJSON(current.toString())
: current
);
}If you control the API contract, identifiers should still be sent as strings. These new APIs are especially useful when consuming an external contract that you cannot modify.
Actual support in current runtimes
An API's presence in the specification does not mean it is available in every environment.
These checks were performed directly on the listed runtimes on July 26, 2026:
| API | Node 22.23 | Node 24.18 | Node 26.5 | Chromium 150 |
|---|---|---|---|---|
Array.fromAsync |
Yes | Yes | Yes | Yes |
Map.getOrInsert |
No | No | Yes | Yes |
Iterator.concat |
No | No | Yes | Yes |
Base64/hex in Uint8Array |
No | No | Yes | Yes |
Error.isError |
No | Yes | Yes | Yes |
Math.sumPrecise |
No | No | No | Yes |
| JSON source / raw JSON | Yes | Yes | Yes | Yes |
The Math.sumPrecise case demonstrates why support should not be assumed based solely on the Node version number: even Node 26 does not implement it yet.
Feature detection
For code that runs across different runtimes:
const support = {
arrayFromAsync:
typeof Array.fromAsync === 'function',
mapGetOrInsert:
typeof Map.prototype.getOrInsert ===
'function',
iteratorConcat:
typeof Iterator !== 'undefined' &&
typeof Iterator.concat === 'function',
uint8ArrayBase64:
typeof Uint8Array.prototype.toBase64 ===
'function',
mathSumPrecise:
typeof Math.sumPrecise === 'function',
errorIsError:
typeof Error.isError === 'function',
rawJson:
typeof JSON.rawJSON === 'function',
};Do not use a fallback that silently changes the result. For critical data, fail explicitly or load a proven polyfill.
Which ones I would use today
In projects where the minimum runtime version is under your control:
Map.getOrInsertremoves a fair amount of repetitive code.- The
Uint8Arrayconversions replace Base64 and hex helpers. Error.isErroris a direct improvement for logging.Array.fromAsyncsimplifies tests and stream consumption.- The JSON APIs solve a data integrity problem.
I would use Iterator.concat when lazy evaluation provides a real benefit. For small arrays, spread syntax remains more familiar:
const combined = [...first, ...second];I would wait before depending on Math.sumPrecise in cross-platform code. Its support is still more limited than the rest of the edition.
Conclusion
ECMAScript 2026 is an edition focused on eliminating small friction points.
It does not change the language's core syntax, but it does reduce the need to maintain custom utilities for:
- Initializing values in Maps.
- Consuming asynchronous iterables.
- Sequencing Iterators.
- Encoding bytes.
- Detecting errors across realms.
- Adding values with less precision loss.
- Preserving large numbers when processing JSON.
The most important part is not memorizing seven new names. It is recognizing which of these APIs replaces real code in your project and checking whether the runtime where you deploy already implements it.