ECMA International approved ECMAScript 2026 on June 30. Nineteen days later, most of its additions already work natively in Node.js 24 and Chrome 137+ — no polyfill, no flag, no waiting. The spec formalizes patterns your team has been writing by hand for years and eliminates a handful of genuinely annoying bugs. Here’s the tier-by-tier breakdown of what you can actually ship today.
Two Tiers, One Decision
ES2026’s features split cleanly by runtime support. Everything in Tier 1 runs on Node 22+, Chrome 121+, and Firefox 115+ — broadly available right now. Tier 2 needs Node 24+ or Chrome 134–144+, and Safari hasn’t shipped most of it yet. If you’re writing server-side Node code, upgrade to 24 and use everything. If you’re targeting browsers, check the table below before you ship.
| Feature | Node 22+ | Node 24+ | Chrome 137+ | Safari |
|---|---|---|---|---|
| Array.fromAsync | Yes | Yes | Yes | Yes (16.4+) |
| Uint8Array Base64/Hex | Yes | Yes | Yes | Yes (16.4+) |
| JSON source text | Yes | Yes | Yes | Yes |
| Error.isError | No | Yes | Yes (134+) | No |
| Math.sumPrecise | No | Yes | Yes | No |
| Map.getOrInsert | No | Yes | Yes (144+) | No |
| Iterator.concat | No | Yes | Yes | No |
Map.getOrInsert: Kill Three Lines of Boilerplate
This is the feature with the most immediate impact on existing code. Every codebase has a variant of this pattern:
if (!productsByCategory.has(product.category)) {
productsByCategory.set(product.category, []);
}
productsByCategory.get(product.category).push(product);
Three lines, two lookups, a mutation. Map.prototype.getOrInsert() collapses it:
productsByCategory.getOrInsert(product.category, []).push(product);
The companion method, getOrInsertComputed(), takes a factory function instead of a value — it only runs when the key is absent. Use this for anything expensive: profile creation, cache initialization, dependency wiring.
const profile = cache.getOrInsertComputed(userId, () => createProfile(userId));
Both methods land on Map and WeakMap. Requires Node 24+ or Chrome 144+. Safari doesn’t have it yet — keep the polyfill for client-side code until it does.
Math.sumPrecise: The Floating-Point Bug You’ve Definitely Hit
This one has a satisfying before/after. Try this in any JavaScript runtime today:
[1e16, 1, -1e16].reduce((a, b) => a + b, 0) // → 0
The correct answer is 1. The large magnitudes absorb the small value during accumulation. Math.sumPrecise() uses compensated summation to avoid this:
Math.sumPrecise([1e16, 1, -1e16]) // → 1
It takes any iterable, including generators, which makes it useful for processing filtered datasets without materializing intermediate arrays:
function* validMeasurements(rows) {
for (const row of rows) {
if (Number.isFinite(row.value)) yield row.value;
}
}
const total = Math.sumPrecise(validMeasurements(sensorData));
One important caveat: this is still IEEE 754 floating-point. It’s more accurate, not exact. If you’re summing currency, store amounts as integer cents and use integer arithmetic. Math.sumPrecise is for sensor data, coordinates, statistics, and scientific calculations — not money. Requires Node 24+ and Chrome 137+. No Safari support yet. See the MDN reference for Math.sumPrecise.
Array.fromAsync: Replace the for-await Loop
The widest compatibility of anything in this spec — Node 22+, all major browsers — and the easiest to adopt. Array.fromAsync() converts an async iterable into a resolved array:
// Before
const pages = [];
for await (const page of loadPages()) {
pages.push(page);
}
// After
const pages = await Array.fromAsync(loadPages());
It also accepts a mapper as the second argument, just like Array.from():
const titles = await Array.fromAsync(loadPages(), page => page.title);
Use it for paginated API responses, database cursors, and streamed records. Note that it consumes the full iterable — if you need to stop early, keep using for await.
Uint8Array Base64 and Hex: Drop the npm Package
Every project that touches crypto, file hashing, or binary API payloads has a base64 or hex utility somewhere. ES2026 makes them native on Uint8Array:
// Hex encode a SHA-256 digest
const digest = await crypto.subtle.digest("SHA-256", encoded);
return new Uint8Array(digest).toHex();
// Base64 encode/decode
const bytes = new Uint8Array([72, 101, 108, 108, 111]);
bytes.toBase64(); // → "SGVsbG8="
Uint8Array.fromBase64("SGVsbG8="); // decode
This is one of the few ES2026 features with good Safari support (16.4+). If your package.json has a base64 utility library purely for browser compatibility, this likely replaces it. Verify your target Safari version before removing the dependency.
Error.isError: Fix the Cross-Realm Blind Spot
instanceof Error lies when the Error object came from a different JavaScript realm — an iframe, a web worker, a Node vm context. The check silently returns false on a legitimate Error. Error.isError() works correctly across all realms:
// Reliable cross-realm detection
if (Error.isError(value)) {
console.error(value.message);
}
// Normalization pattern for libraries
function normalize(val) {
if (Error.isError(val)) return val;
return new Error(typeof val === 'string' ? val : 'Unknown error', { cause: val });
}
If you maintain a component library, an SDK, or anything that runs in an iframe context, this is a direct upgrade. Node 24+, Chrome 134+, Firefox 139+. Safari has no support yet.
Iterator.concat: Merge Without Materializing
Iterator.concat() sequences multiple iterables lazily — it doesn’t spread them into an array first. This matters when you’re searching through large or expensive sources and want to stop early:
// Eager - both sources fully evaluated
const all = [...localResults(), ...remoteResults()];
// Lazy - stops when a match is found
const all = Iterator.concat(localResults(), remoteResults());
for (const item of all) {
if (item.matches(query)) break; // remoteResults() may never run
}
Node 24+, Chrome 137+, Firefox 136+. Safari: no support. Useful for search pipelines, navigation menus from mixed sources, and anywhere you chain generators today with a spread operator.
JSON Source Text Access: Preserve BigInt Precision
JavaScript’s JSON.parse silently corrupts large integers that exceed Number.MAX_SAFE_INTEGER. ES2026 adds a source property to the reviver callback so you can intercept the raw string before it’s converted:
const result = JSON.parse('{"id":123456789012345678901234567890}',
(key, value, { source }) => {
if (key === 'id') return BigInt(source);
return value;
}
);
The companion JSON.rawJSON() lets you serialize BigInt values without a custom replacer. If you work with database IDs, blockchain transaction hashes, or any 64-bit integer system, this integrates immediately. Support is broad — Node 22+, Chrome 121+, Firefox 115+.
What’s Still Waiting
Temporal — the replacement for Date — is available in Chrome 144+ and Firefox, but Safari only has partial Technical Preview support. Don’t build new date logic on Temporal for user-facing code until Safari ships a stable release. Server-side Node 24 code can use it today.
Explicit Resource Management (the using keyword) is Stage 4 and runs in Chrome and Node, but Firefox support remains behind a flag. It did not make it into the official ES2026 spec. Expect it in ES2027.
Upgrading
Check your Node version:
node --version
Node 24 is the current release line and ships all ES2026 features natively. Node 22 (LTS) covers Tier 1 only. If you need Tier 2 features on Node 22, core-js v3.38+ provides polyfills for Math.sumPrecise, Map.getOrInsert, and Iterator.concat. The full ECMAScript 2026 approval announcement covers the complete feature list, and the official TC39 specification is the authoritative reference.
Feature detect before relying on availability in mixed environments:
const hasPreciseSum = typeof Math.sumPrecise === 'function';
const hasGetOrInsert = Map.prototype.getOrInsert !== undefined;
const hasErrorCheck = typeof Error.isError === 'function';













