12k
All articles

What's New in ECMAScript 2026

ES2026 adds seven JavaScript features: Array.fromAsync, Math.sumPrecise, Uint8Array base64 and hex methods, JSON.parse source access, and getOrInsert.

OpenReplay Team
OpenReplay Team
What's New in ECMAScript 2026

Ecma International approved the ECMAScript 2026 language specification on 30 June 2026, and it contains exactly seven new features: Array.fromAsync, Error.isError, Math.sumPrecise, base64 and hex methods on Uint8Array, Iterator.concat, JSON.parse source text access, and getOrInsert on Map and WeakMap.

Every year these methods land quietly, and most of us meet them on the day we delete a helper we had been carrying around for years. This time it is probably a base64 utility.

If you have searched for ES2026 recently, you have probably found lists that disagree with each other, several of which include Temporal or the using keyword. Those lists are wrong, and this article explains why, then walks through each of the seven features that actually shipped: the problem it solves, the code it replaces, and the one-liner that replaces it.

The authoritative record is Ecma’s approval announcement, which confirms ES2026 as the 17th edition of ECMA-262, and TC39’s finished-proposals table, where exactly seven rows carry an Expected Publication Year of 2026.

Key Takeaways

  • ES2026 was approved on 30 June 2026 and adds exactly seven features; Temporal and Explicit Resource Management are not among them.
  • Math.sumPrecise([1e17, 1, -1e17]) returns 1, while the same values summed with reduce return 0.
  • Uint8Array gains built-in toBase64(), toHex(), fromBase64(), and fromHex(), removing a common dependency.
  • The JSON.parse reviver now receives a third argument exposing the raw source text, and JSON.rawJSON() fixes the stringify side, so BigInts can round-trip through JSON.
  • Map.prototype.getOrInsert(key, value) returns the existing value when the key is already present; it only inserts your default when the key is absent.

What Did Not Make ES2026?

Temporal and Explicit Resource Management (using / await using) are not part of ES2026. On the TC39 finished-proposals list, both carry an Expected Publication Year of 2027, alongside Atomics.pause and Joint Iteration. All four reached stage 4, the final step of the TC39 process, after the ES2026 cutoff. Pre-approval coverage from late 2025 widely predicted they would land in 2026, which is where the conflicting feature lists come from. The check anyone can run: filter the Expected Publication Year column of the finished-proposals table. Seven proposals read 2026, and none of them is Temporal.

Array.fromAsync

Array.fromAsync takes an async iterator and gives you back an array, wrapped in a promise. It also accepts a sync iterator whose values are promises, an array-like object, and an optional mapping function, the same way Array.from does. Until now, collecting an async iterator meant a for await loop pushing into a mutable array:

const arr = [];
for await (const n of asyncGen(1, 3)) {
  arr.push(`page ${n}`);
}

The same result is now a single expression:

const arr = await Array.fromAsync(asyncGen(1, 3), (n) => `page ${n}`);
// [ 'page 1', 'page 2', 'page 3' ]

Error.isError

Error.isError(value) returns true only when the caught value is a genuine Error object. JavaScript lets you throw anything, and instanceof Error misleads once values cross iframe or realm boundaries, because each realm has its own Error constructor. A catch block gated on instanceof Error can silently skip the branch that renders a useful message, and session replays of that bug class show a familiar shape: the user stuck on a screen that reports nothing while a real throw happened underneath. Error.isError closes that gap.

try {
  throw new Error("session upload failed");
} catch (error) {
  Error.isError(error); // true
}

try {
  throw "session upload failed";
} catch (error) {
  Error.isError(error); // false
}

Math.sumPrecise

Math.sumPrecise sums an iterable of numbers without accumulating floating-point error in the intermediate results. A reduce chain adds pairwise, so large and small magnitudes cancel badly:

const values = [1e17, 1, -1e17];

values.reduce((a, b) => a + b, 0); // 0
Math.sumPrecise(values);           // 1

The reduce version loses the 1 entirely. Beyond precision, this also retires the sum reducer as boilerplate.

Uint8Array toBase64, toHex, fromBase64, and fromHex

ES2026 gives Uint8Array built-in toBase64() and toHex() methods, plus static Uint8Array.fromBase64() and Uint8Array.fromHex() for decoding. Binary-to-text conversion previously meant pulling in a library or hand-rolling the encoding; this is a dependency most codebases can now delete.

const value = new Uint8Array([79, 112, 101, 110, 82, 101, 112, 108, 97, 121]);

const valueBase64 = value.toBase64(); // 'T3BlblJlcGxheQ=='
const decoded = Uint8Array.fromBase64(valueBase64);

new TextDecoder().decode(decoded); // 'OpenReplay'

Iterator.concat

Iterator.concat() chains iterators in sequence. Until now the only way to do that was a hand-written generator that walked each source and handed off with yield*:

function* combine(...iterators) {
  for (const source of iterators) {
    yield* source;
  }
}

Now it is a single call, and plain arrays of values can slot in between iterators. The [300] in the middle below is a plain array, and it needs no wrapping in Iterator.from.

const iOne = Iterator.from([100, 200]);
const iTwo = Iterator.from([400, 500]);

const combined = Iterator.concat(iOne, [300], iTwo);
Array.from(combined); // [ 100, 200, 300, 400, 500 ]

JSON.parse Source Text Access

JSON round-tripping in JavaScript was lossy in both directions before ES2026. Parsing a large integer literal silently rounds it, and stringifying a BigInt throws:

JSON.parse("9007199254740993");   // 9007199254740992
JSON.stringify(9007199254740993n); // TypeError: Do not know how to serialize a BigInt

The JSON.parse source text access proposal fixes both sides. JSON.parse now hands its reviver a third argument holding the untouched source text, which you can convert yourself without losing digits. On the stringify side, JSON.rawJSON() lets a replacer emit a value verbatim:

JSON.parse("9007199254740993", (key, value, { source }) => BigInt(source));
// 9007199254740993n

JSON.stringify(9007199254740993n, (key, value) => JSON.rawJSON(value));
// 9007199254740993

A BigInt can now survive a JSON round trip without losing digits or throwing.

Map.prototype.getOrInsert and WeakMap.prototype.getOrInsert

The Upsert proposal brings getOrInsert(key, value) to both Map and WeakMap, so you no longer need to call has() and then set(). The semantics matter: getOrInsert returns the existing value when the key is already present. It only inserts, and only returns your default, when the key is absent.

const limits = new Map();
limits.set("uploads", 25);

limits.getOrInsert("exports", 10);   // 10  (inserted)
limits.getOrInsert("uploads", 100);  // 25  (existing value wins)

Note that 100 is discarded because "uploads" was already set. Treat the second argument as a default, not an overwrite.

What Can You Delete Now?

ES2026 is a cleanup release: every one of the seven features retires a pattern you probably have in production today. The base64 helper dependency, the sum reducer, the has()-then-set() guard blocks, the yield* concatenation generator, and the for await collection loop all have direct built-in replacements. Runtime availability varies by engine, so check MDN’s standard built-in objects reference for the current support picture of each method before dropping polyfills. Then start with the easiest win in your codebase, which for most teams is swapping the Map existence checks for getOrInsert.

FAQs

What is the difference between Array.fromAsync and Promise.all?

Array.fromAsync awaits values sequentially, one at a time, and accepts async iterables, sync iterables that yield promises, and array-like objects. Promise.all runs an iterable of promises concurrently and rejects as soon as any promise rejects. Use Promise.all for parallel work and Array.fromAsync when order matters or the source is an async iterator. Both return a promise that resolves to an array.

Does Error.isError return true for TypeError and other Error subclasses?

Yes. Error.isError returns true for every genuine Error subclass, including TypeError, RangeError, SyntaxError, and custom classes that extend Error, because engines check an internal brand that real errors carry rather than the prototype chain. In browsers, a DOMException counts as a real error too. It returns false for objects that merely mimic errors, such as plain objects with a message property or objects created with Object.create(Error.prototype).

Can Uint8Array.prototype.toBase64 produce URL-safe base64?

Yes. toBase64 accepts an options object with an alphabet property set to 'base64' (the default, using + and /) or 'base64url' (using - and _), plus an omitPadding boolean that drops trailing = characters. Uint8Array.fromBase64 accepts the same alphabet option and a lastChunkHandling option ('loose', 'strict', or 'stop-before-partial') that controls how a final chunk shorter than four characters is decoded.

How do I avoid computing an expensive default value with getOrInsert?

Use getOrInsertComputed(key, callback), which the same Upsert proposal adds to both Map and WeakMap. It calls the callback to produce the value only when the key is absent, then inserts and returns the result; when the key already exists, the callback never runs and the existing value is returned. Plain getOrInsert always evaluates its second argument before the call, even when the key is already present.

Open-source session replay

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.