Checklist: What shipped in ES2026, and what to wait on. ES2024 / ES2025 items (groupBy, Set methods, Promise.try): already baseline (pass); Array.fromAsync, Error.isError: adopt now (pass); Iterator.concat, Map.getOrInsert, Uint8Array base64: polyfill old targets (partial); Math.sumPrecise, JSON.parse source: polyfill or wait (partial); Temporal, using: finished, but the spec labels them ES2027 (fail); Pipeline operator (|>): Stage 2, do not ship (fail).

Modern ECMAScript for 2026: the JavaScript features that shipped, and which to adopt

A runtime-targeted guide to the seven features that actually landed in ES2026, the ES2024/ES2025 items every roundup keeps re-listing as new, and the finished-but-ES2027 proposals people mislabel as shipped. Each verdict is bound to your lowest-supported runtime.

Why most "2026 features" roundups are quietly wrong#

This guide is a rebuild of a post I wrote in June 2023 about ECMAScript 2023. The spirit is the same, useful to a novice and to an expert at once, but the feature set has turned over, and so has a problem worth naming.

Most roundups that promise "the new JavaScript features for 2026" conflate three different things. They re-list features that shipped years ago and are already baseline. They correctly name the handful that are genuinely new in the ES2026 edition. And they file finished-but-not-2026 proposals under 2026 as if the label were decoration. The result reads authoritative and sends you to adopt things on the wrong timeline.

The fix is to separate the edition year from the ship date, and both from the TC39 Stage. A proposal is final when it reaches Stage 4. The edition year (ES2025, ES2026, ES2027) is a yearly bookkeeping snapshot of what was final by a cutoff, and engines often ship a feature well before or after that label. MDN Baseline tells you where it actually runs. Get those three axes straight and the adoption call stops being a guess.

What actually shipped: Stage and edition for every feature people call "2026"#

Here is the single classification that corrects the error. Find a feature, read its bucket, and act on the bucket, not on a headline. The three ES2024/ES2025 rows are the ones roundups most often re-sell as new; the four buckets are the whole story.

TC39 Stage and ECMAScript edition for the features commonly filed under 2026
FeatureTC39 StageECMAScript editionBucket
Object.groupBy / Map.groupByStage 4ES2024Baseline, use freely
Promise.withResolversStage 4ES2024Baseline, use freely
Iterator helpers (.map / .filter / .take)Stage 4ES2025Baseline, use freely
Set methods (union / intersection / difference)Stage 4ES2025Baseline, use freely
Promise.tryStage 4ES2025Baseline, use freely
RegExp.escapeStage 4ES2025Baseline, use freely
Import attributes (import ... with)Stage 4ES2025Baseline, use freely
JSON modulesStage 4ES2025Baseline, use freely
Float16ArrayStage 4ES2025Baseline, use freely
Array.fromAsyncStage 4ES2026New in 2026
Iterator.concatStage 4ES2026New in 2026
Map.getOrInsertStage 4ES2026New in 2026
Math.sumPreciseStage 4ES2026New in 2026
Error.isErrorStage 4ES2026New in 2026
Uint8Array Base64 / hexStage 4ES2026New in 2026
JSON.parse source accessStage 4ES2026New in 2026
TemporalStage 4ES2027Finished, but not 2026
Explicit resource management (using)Stage 4ES2027Finished, but not 2026
Pipeline operator (|>)Stage 2None yetIn flight, do not ship

Two rows deserve a flag before anyone quotes them back at you. Temporal and explicit resource management are done as proposals, which is why so many posts grab them, but the spec slots them into the ES2027 edition and the engines are still catching up. The pipeline operator is not final at all. It is Stage 2, its syntax can still change, and it belongs nowhere near production.

Can you actually ship it? The runtime-targeted decision engine#

Knowing a feature is "new in 2026" does not tell you whether you can ship it. That depends on the oldest runtime you still support. Walk any feature down this flow and you land on one of three verdicts: adopt now, ship it behind a polyfill, or wait. This is the engine the rest of the post feeds.

From a feature to an adopt / polyfill / wait verdictStart with a feature and answer four questions in order: is it Stage 4, is it in ES2026 or earlier, how does its MDN Baseline status compare to your lowest-supported runtime, and do you still support versions older than its ship date. The branches route to adopt now, polyfill, or wait.

The questions run cheapest to most expensive. You only step down a branch when the previous answer forces you to. A feature that is baseline and widely available needs no polyfill and no thought. A feature that is finished but slated for ES2027 is a deliberate polyfill-or-wait decision, not an accident.

Where each of the seven is supported: Baseline plus Node, Deno, and Bun#

This is the lookup companion to the flow above. It binds each verdict to a concrete runtime instead of a vague "use a transpiler". Read your lowest-supported runtime across the row and the verdict falls out.

ES2026 features: MDN Baseline and minimum server-runtime versions (snapshot, July 2026)
FeatureMDN BaselineNodeDenoBunVerdict
Array.fromAsyncWidely available22+1.38+1.0+Adopt now
Error.isErrorWidely available22+1.38+1.2+Adopt now
Uint8Array Base64 / hexNewly available24+2.0+1.2+Adopt, or polyfill for old browsers
Iterator.concatNewly available24+2.0+1.2+Polyfill on older targets
Map.getOrInsertNewly available24+2.1+1.2+Polyfill on older targets
Math.sumPreciseLimited24+2.1+1.2+Polyfill (small) or wait
JSON.parse source accessLimited22+1.40+1.2+Polyfill or wait

Widely available means every major engine has shipped it and enough time has passed that you can rely on it. Newly available means it is in every major engine but only recently, so it is safe on servers you control and on evergreen browsers, and needs a polyfill only if you still support older browser versions. Limited means at least one engine is missing it, which turns the call into polyfill-or-wait. The rest of the post takes the seven a few at a time.

Array.fromAsync and Iterator.concat: streaming and sequencing without the boilerplate#

Array.fromAsync is Array.from for async iterables. Point it at anything you would normally drain with a for await loop, a paginated API, a database cursor, a byte stream, and get back a promise for the collected array. For a novice: it turns a five-line accumulation loop into one line. For the expert: it awaits sequentially and preserves back-pressure, so it will not fire every request at once the way naively mapping to promises would.

js
// Before: collect an async iterable by hand
const rows = [];
for await (const row of streamRows()) {
  rows.push(row);
}

// After: one call, same result, back-pressure preserved
const rows = await Array.fromAsync(streamRows());

Iterator.concat is the sync counterpart to a job people kept doing with the spread operator. Spreading three iterables into one array copies all of them up front. Iterator.concat returns a lazy iterator that walks each source in order and pulls values only as you ask for them, so if you stop early the later sources are never touched. That laziness is the point: it composes with the ES2025 iterator helpers and never materializes data you do not read.

js
// Before: spread each source into one array first.
// This materializes all three up front, even if you only read the first item.
const all = [...pageA, ...pageB, ...pageC];

// After: walk each iterator in sequence, lazily.
// Nothing is copied; items are pulled from A, then B, then C, on demand.
for (const item of Iterator.concat(pageA, pageB, pageC)) {
  handle(item);
  if (done()) break; // pages B and C are never touched
}

Map.getOrInsert: the upsert you kept hand-writing#

Every codebase has the same three-line pattern: check whether a key is in a Map, insert a default if it is not, then use the value. Map.getOrInsert collapses it into one call and one lookup. Its sibling getOrInsertComputed takes a callback instead of a value, so an expensive default is built only on a miss. The win here is code you stop repeating and a class of "forgot to set it back" bugs that disappears.

js
// Before: the has / get / set dance, three lookups for one upsert
let list = groups.get(key);
if (list === undefined) {
  list = [];
  groups.set(key, list);
}
list.push(item);

// After: one call
const list = groups.getOrInsert(key, []);
list.push(item);

// When the default is expensive to build, defer it.
// The callback runs only on a miss.
const parsed = cache.getOrInsertComputed(key, () => expensiveParse(key));

Math.sumPrecise: the floating-point bug you have been shipping#

Add an array of floating-point numbers with reduce and the result drifts. Not because your code is wrong, but because each intermediate sum rounds to the nearest double and the errors accumulate. For most arrays it is invisible. For a money column, a long series, or values that span very different magnitudes, it is a real bug that reaches production. Math.sumPrecise computes the correctly rounded sum of the whole array in one pass, no drift.

js
[1e20, 3.14, -1e20].reduce((a, b) => a + b, 0); // 0   (wrong)
Math.sumPrecise([1e20, 3.14, -1e20]);              // 3.14 (correct)

The clearest way to feel it is to watch the two results move. Pick a preset or type your own array. The left card is the naive reduce, the right card is the exact sum. When they disagree the panel calls out the drift.

Naive reduce sum vs Math.sumPrecise
Naive reduce sum0.30000000000000004values.reduce((a, b) => a + b, 0)
Math.sumPrecise0.30000000000000004Math.sumPrecise(values)

These match for this input. Naive summation still fails once magnitudes differ enough, so try a preset.

Pick a preset or edit the array. The two cards recompute live: the naive sum drifts on the presets, the exact sum does not. The starting numbers render before any JavaScript runs, so the comparison is readable even with scripting off.

Where the runtime does not yet expose Math.sumPrecise, the demo falls back to the same exact-summation algorithm the proposal specifies, so the number on the right is always the correct answer, never a second guess at the wrong one.

Uint8Array Base64/hex and JSON.parse source access: delete the polyfill#

Two of the seven exist to remove code you should never have had to write. Encoding a byte array to base64 in the browser was a small ritual of btoa, String.fromCharCode, and edge-case bugs on binary data, or a dependency you pulled in to avoid them. Uint8Array.prototype.toBase64 and Uint8Array.fromBase64 (with hex variants) do it natively and correctly. Separately, JSON.parse now passes the reviver the exact source text of each value, which finally lets you preserve a large integer without it round-tripping through a lossy double.

Hand-rolled base64 vs the native Uint8Array method~18x less code

~18 lines + ~2 KB dep

Hand-rolled encoder or small dependency

Native

1 line + 0 added bytes

Native Uint8Array.toBase64()

Beyond the line count, the native method is correct on arbitrary binary, where hand-rolled encoders quietly corrupt bytes above the ASCII range. You delete a dependency, ship zero added bytes, and remove a bug class at the same time. Line counts are typical for a hand-rolled encoder, not a fixed measurement.

Hand-rolled base64 vs the native Uint8Array method (lines of code to maintain)
Optionlines of code to maintain
Hand-rolled encoder or small dependency~18 lines + ~2 KB dep
Native Uint8Array.toBase64()1 line + 0 added bytes

Source: MDN: Uint8Array.prototype.toBase64 (line counts illustrative)

js
// Before: a hand-rolled base64 encoder (~18 lines) or a small dependency
import { encode, decode } from 'some-base64-lib';
const token = encode(bytes);

// After: native, no dependency, correct on arbitrary binary
const token = bytes.toBase64();
const roundTrip = Uint8Array.fromBase64(token);

// JSON.parse now hands the reviver the exact source text of each value,
// so a large integer survives without going through a lossy double.
const data = JSON.parse(text, (key, value, { source }) =>
  key === 'id' ? BigInt(source) : value,
);

Error.isError: the realm-safe type check#

The seventh feature is small and fixes a sharp edge. value instanceof Error returns false for an error that came from another realm, an iframe, a worker, or a vm context, because each realm has its own Error constructor. Error.isError checks the internal brand instead, so it recognizes an error regardless of where it was created. If you write libraries, handle errors from workers, or run untrusted code in a sandbox, this is the check you actually wanted.

js
// instanceof lies across realms (an iframe, a worker, a vm context):
// an Error from another realm is not an instanceof YOUR realm's Error.
errFromWorker instanceof Error; // false, even though it is an Error

// Error.isError checks the internal brand, so it is realm-safe.
Error.isError(errFromWorker); // true

The mislabel trap: Temporal and using are finished, but they are ES2027#

These two are the sharpest correction in this guide, because they are the two roundups get most confidently wrong. Both reached Stage 4, so calling them "finished" is fair. But the spec files them in the ES2027 edition, not 2026, and engine support is still rolling out. A post that lists them under "new in ES2026" is telling you to ship on a timeline the engines have not met.

js
// Temporal: an immutable, typed replacement for the Date footguns.
const start = Temporal.PlainDate.from('2026-07-07');
const due = start.add({ days: 30 }); // no mutation, no month-is-zero surprise

// Explicit resource management: 'using' calls [Symbol.dispose]() at block exit.
function readConfig() {
  using file = openSync('./config.json'); // released when the block ends
  return JSON.parse(file.readText());
} // file is disposed here, on every exit path, including a throw

Temporal is the immutable, typed replacement for Date, and it is genuinely worth wanting. Explicit resource management adds the using declaration, which calls a value's [Symbol.dispose]() when the block exits, on every path including a thrown error, so files, locks, and handles close without a try/finally. Both are safe to adopt only behind a deliberate polyfill today. Temporal's polyfill is not small, so measure the bundle cost against how much you need it, and treat the decision as exactly that: a decision, not a default.

These are language features, not a stack decision. If the question underneath is which language should run your server at all, our comparison of where PHP and JavaScript each still win for web work covers that layer instead. And if you are about to spend these features on a browser app, building a real Progressive Web App with React shows what the runtime demands once the code actually ships.

When not to reach for the 2026 seven#

The honest default is boring: adopt the widely-available features now, gate the newly-available and limited ones on your real support matrix, and leave the ES2027 and Stage 2 items alone until the engines catch up. That is less exciting than a headline promising twenty new features, and it is the version that does not page you at 2am.

Where to go next#

Picking features by runtime is one slice of shipping JavaScript that holds up. If you are choosing how to render and deploy the app those features run in, the companion piece covers rendering a React app on Cloudflare across static, server, and edge. If raw speed is the goal, here is how we approach Core Web Vitals and performance, and the broader practice of software engineering behind a launch. For the full reference on what is final, the TC39 finished-proposals list is the source of truth this post is checked against.

If you want another pair of hands on a JavaScript codebase you are modernizing, you can hire JavaScript developers from our team, or talk to us about the fit. No pressure and no lock-in: everything above is standard, documented web platform work you own outright.

Portrait of Tirth Bodawala

Tirth Bodawala

Chief Technology Officer, Atyantik Technologies

Tirth leads software engineering at Atyantik Technologies, a software product studio building web platforms, mobile apps, and AI-integrated systems since 2015. He writes about shipping software that holds up in production, from the language features you can trust to the platform decisions behind a launch.

More from Tirth BodawalaAbout AtyantikHire JavaScript developers

Keep reading