Frontend Labs
⋯

55. JavaScript in 2026: the features worth adopting now

TC39 publishes a new edition of ECMAScript every June. A feature joins the edition after the year it reaches stage 4: finished, with two shipping implementations. Here is what ES2023–ES2026 added, grouped by the problems it solves. Everything below runs in current Chrome, Edge and Firefox. Safari support is noted where it lags.

Immutable updates without libraries (ES2023)

const orders = [
  { id: 1, status: "paid", total: 40 },
  { id: 2, status: "refunded", total: 15 },
  { id: 3, status: "paid", total: 99 },
];
const byTotal = orders.toSorted((a, b) => b.total - a.total);   // copy, sorted
const renamed = orders.with(1, { ...orders[1], status: "void" }); // copy, one item replaced
orders[1].status;                    // "refunded": the original is untouched
orders.findLast((o) => o.status === "paid").id;   // 3

toSorted, toReversed, toSpliced and with are the non-mutating twins of sort, reverse, splice and index assignment. They are exactly what React and Redux state updates need.

Grouping and promise plumbing (ES2024–ES2025)

Object.groupBy(orders, (o) => o.status);    // { paid: [...2], refunded: [...1] }
Map.groupBy(orders, (o) => o.total > 50);   // Map { false => [...], true => [...] }

// Promise.withResolvers: create a promise now, settle it from somewhere else
const { promise, resolve, reject } = Promise.withResolvers();
socket.addEventListener("message", (e) => resolve(e.data), { once: true });
await promise;

// Promise.try: sync throws and async rejections share one error path
const parse = (s) => Promise.try(() => JSON.parse(s));
parse("{bad").catch((e) => e.name);          // "SyntaxError", no try/catch needed

Sets that do set algebra (ES2025)

const canEdit = new Set(["alice", "bob", "carol"]);
const online = new Set(["bob", "dave"]);
canEdit.intersection(online);        // Set {"bob"}
canEdit.difference(online);          // Set {"alice", "carol"}
canEdit.union(online);               // Set {"alice", "bob", "carol", "dave"}
online.isSubsetOf(canEdit);          // false

Strings, regexes and numbers

// ES2025: RegExp.escape — put user input inside a pattern safely (and avoid ReDoS-by-accident)
new RegExp(RegExp.escape("C++ (2024)"), "i").test("I learnt c++ (2024) at school");  // true

// ES2025: the same named group in different alternatives
const date = /(?<y>\d{4})-(?<m>\d{2})|(?<m>\d{2})\/(?<y>\d{4})/;
"2026-10".match(date).groups.y;      // "2026"
"10/2026".match(date).groups.y;      // "2026"

// ES2025: half-precision floats, for ML weights and WebGPU buffers (§57)
new Float16Array([0.1])[0];          // 0.0999755859375: 2 bytes per number

// ES2026: Math.sumPrecise — no accumulated floating-point error
[0.1, 0.2, 0.3].reduce((a, b) => a + b);   // 0.6000000000000001
Math.sumPrecise([0.1, 0.2, 0.3]);          // 0.6

ES2026: the June 2026 edition

The 2026 edition has seven features. Six have practical uses in apps:

// Base64 and hex, built in (no more btoa(String.fromCharCode(...bytes)) hacks)
const bytes = new TextEncoder().encode("héllo");
bytes.toBase64();                                         // "aMOpbGxv"
bytes.toHex();                                            // "68c3a96c6c6f"
Uint8Array.fromBase64("aMOpbGxv");                        // back to bytes
new Uint8Array(4).toBase64({ alphabet: "base64url", omitPadding: true }); // "AAAAAA": JWT/WebAuthn style

// Error.isError: true for real errors, even from other realms (iframes, workers)
Error.isError(new TypeError("x"));                        // true
Error.isError({ message: "fake", stack: "" });            // false

// Map.prototype.getOrInsert / getOrInsertComputed: "upsert" in one call
const byOwner = new Map();
for (const [owner, id] of rows) byOwner.getOrInsert(owner, []).push(id);
cache.getOrInsertComputed(url, (key) => expensiveParse(key));   // computed only on a miss

// Iterator.concat: chain iterables lazily (pass objects; strings are rejected)
Iterator.concat([1, 2], new Set([3]), map.keys()).toArray();

// JSON.parse source text access: big integers without losing digits
JSON.parse('{"id": 12345678901234567890}').id;            // 12345678901234567000  ✗
JSON.parse('{"id": 12345678901234567890}', (k, v, { source }) => (k === "id" ? BigInt(source) : v)).id;
// 12345678901234567890n  ✓  (database IDs, Twitter/X snowflakes, money in minor units)

// Array.fromAsync: see §54

Finished and shipping, in the 2027 edition: Temporal and using

Two large features reached stage 4 too late for the 2026 edition. Both already ship in Chrome and Firefox and are safe to use with a fallback.

Temporal replaces Date. Its values are immutable, time zones and calendars are first-class, and it has separate types for separate ideas: PlainDate (a birthday), ZonedDateTime (a meeting), Instant (a log timestamp), and Duration. Support: Firefox 139+, Chrome/Edge 144+. Safari was still in Technology Preview at the time of writing, so ship the official @js-temporal/polyfill behind feature detection.

const meeting = Temporal.ZonedDateTime.from("2026-03-28T09:00[America/New_York]");
meeting.withTimeZone("Europe/London").toString();
// "2026-03-28T13:00:00+00:00[Europe/London]": the US is on DST, the UK isn't yet
meeting.add({ days: 1 }).hour;                       // 9: "same time tomorrow", DST-safe
Temporal.PlainDate.from("2026-01-31").add({ months: 1 }).toString(); // "2026-02-28": clamps
Temporal.PlainDate.from("2026-10-04").until("2026-12-25").days;      // 82
Temporal.Duration.from({ minutes: 135 }).round({ largestUnit: "hours" }).toString(); // "PT2H15M"

War story. Every "same time next week" bug in a scheduling product is a Date bug: date.setDate(date.getDate() + 7) silently moves meetings by an hour across a DST change, and new Date("2026-03-08") is midnight UTC, which is the previous day in New York. Teams worked around this with moment.js (now in maintenance mode), then date-fns and Luxon. Temporal makes those libraries optional for new code.

Explicit resource management adds using and await using. They declare a block-scoped constant whose [Symbol.dispose]() (or [Symbol.asyncDispose]()) runs when the block exits, in reverse order, even when an exception is thrown. It is try/finally without the nesting, like C#'s using, Python's with, or Go's defer. Support: Chrome 134+, Firefox 141+, Node 24+. Safari is partial; use TypeScript 5.2+ or Babel to downlevel.

function openResource(name) {
  console.log("open", name);
  return { name, [Symbol.dispose]() { console.log("close", name); } };
}
try {
  using db = openResource("db");
  using lock = openResource("lock");
  throw new Error("boom");
} catch (e) {
  console.log("caught", e.message);
}
// open db · open lock · close lock · close db · caught boom

// DisposableStack collects cleanups dynamically, like Go's defer:
{
  using stack = new DisposableStack();
  const timer = setInterval(poll, 1000);
  stack.defer(() => clearInterval(timer));
  const ctrl = stack.adopt(new AbortController(), (c) => c.abort());
  // …use timer and ctrl…
}   // leaving the block: abort(), then clearInterval(): reverse order

In browser code the obvious targets are event-listener subscriptions, ReadableStream readers (call releaseLock), IndexedDB transactions you want to abort on error, and Web Locks.

Modules: import attributes and JSON modules (ES2025)

import config from "./config.json" with { type: "json" };   // a module, parsed once, cached
const { default: theme } = await import("./theme.json", { with: { type: "json" } });

The type attribute is a security feature. Without it, a server could answer a "JSON" import with JavaScript, and the browser would execute it.

Still in the pipeline (don't ship without a compiler)

  • Decorators (@logged class X {}) are stage 3, and TypeScript 5+ and Babel compile them. No browser ships them natively yet, so they stay a build-time feature.
  • Signals (stage 1) would standardize the reactivity primitive that §71 builds by hand.
  • import defer, AsyncContext (context that follows async calls, like Node's AsyncLocalStorage) and pattern matching are moving through the stages. Follow tc39.es/proposals.

How to check before you ship. Look the feature up on MDN and check its Baseline badge. "Baseline widely available" means it has worked in all major browsers for 30 months. Then decide between a polyfill (core-js, the Temporal polyfill), a compiler (TypeScript, Babel, SWC, esbuild targets), or if ("x" in y) feature detection.

Generated from real-life-JS-guide.md · 8 Oct 2026 16:36