webdev.complete
🆕 Modern JavaScript
JavaScript
Lesson 40 of 117
20 min

What's New (2024-2026)

Set methods, Object.groupBy, Promise.withResolvers, Temporal.

TC39 ships a JavaScript spec every year, ratified each June. The last few rounds have been unusually good: tiny conveniences that delete chunks of code you used to write. Here's a guided tour of the wins from ES2024 through ES2026, the current edition, so you stop polyfilling things that are already built in.

Ratified is not the same as available
The year in the edition name tells you when TC39 signed it off, not when you can use it. Some features (Array.fromAsync) shipped in browsers years before they were ratified. Others sit at Stage 4, implemented and finished, waiting for the next edition to be cut. Check support, not the edition number.

Object.groupBy and Map.groupBy (ES2024)

The bucket-by-key reduce you write every other day is now a method:

js
const items = [
  { name: "apple",  type: "fruit" },
  { name: "carrot", type: "veg"   },
  { name: "pear",   type: "fruit" },
];

Object.groupBy(items, x => x.type);
// {
//   fruit: [{name:"apple",...}, {name:"pear",...}],
//   veg:   [{name:"carrot",...}]
// }

// Need a Map (e.g. for non-string keys)?
Map.groupBy(items, x => x.type);

Promise.withResolvers (ES2024)

Build a promise plus its resolve and reject handles in one line, without the closure ceremony:

js
const { promise, resolve, reject } = Promise.withResolvers();

button.addEventListener("click", () => resolve("clicked!"));

await promise;  // "clicked!"

Promise.try (ES2025)

Run a function that might be sync or async and treat both the same:

js
// Wraps the call in a promise so sync throws and async rejections
// both end up in .catch
Promise.try(() => doMaybeAsyncThing())
  .then(value => /* ... */)
  .catch(err   => /* ... */);

Set methods (ES2025)

Sets finally got the math operations you've been writing by hand:

js
const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);

a.union(b);              // Set {1, 2, 3, 4}
a.intersection(b);       // Set {2, 3}
a.difference(b);         // Set {1}
a.symmetricDifference(b);// Set {1, 4}

a.isSubsetOf(b);         // false
a.isSupersetOf(b);       // false
a.isDisjointFrom(b);     // false

Iterator helpers (ES2025)

Array methods on lazy iterators (see the iterators lesson for more):

js
function* naturals() {
  let n = 1;
  while (true) yield n++;
}

naturals()
  .filter(n => n % 2 === 0)
  .map(n => n * n)
  .take(5)
  .toArray();   // [4, 16, 36, 64, 100]

RegExp.escape (ES2025)

Stop hand-escaping user input before stuffing it into a regex:

js
const userInput = "1 + 2 * 3";
const pattern = new RegExp(RegExp.escape(userInput), "g");

// Without RegExp.escape, the special chars (+ * .) would blow up the regex

AbortSignal.any and AbortSignal.timeout

js
// One signal that fires when ANY of these abort
const signal = AbortSignal.any([
  userCancel.signal,
  AbortSignal.timeout(5000),
]);

await fetch("/api/data", { signal });

structuredClone

Real deep clone, built in. Handles dates, maps, sets, typed arrays, and cyclic references:

js
const original = { a: 1, when: new Date(), nested: { b: 2 } };
const clone = structuredClone(original);

clone.nested.b = 99;
original.nested.b;  // 2 - unchanged
structuredClone doesn't clone functions, DOM nodes, or class instances (those become plain objects). For normal data shapes, it's the answer.

Temporal (shipping, but not everywhere)

Date is universally hated. Temporal is its replacement: timezone-aware, immutable, and it separates wall time from instants instead of pretending they are the same thing. It has gone from "someday" to "in two of three engines".

  • Firefox 139 shipped it first, in May 2025.
  • Chrome and Edge 144 followed in January 2026.
  • Safari has not shipped it. WebKit still lists it as in development.
  • Node 26 and Deno 2.7 have it.
So you still need the polyfill
One holdout engine means Temporal is not Baseline, and Safari is not a browser you get to ignore. Server-side on Node 26 you can use it directly. For anything that runs in a browser, ship the polyfill and write the real API, which at least means you delete the polyfill later instead of rewriting date logic.
js
const now = Temporal.Now.zonedDateTimeISO("America/New_York");
const tomorrow = now.add({ days: 1 });

const birthday = Temporal.PlainDate.from("1990-04-12");
const age = Temporal.PlainDate.from(Temporal.Now.plainDateISO())
  .since(birthday, { largestUnit: "years" }).years;

Object.hasOwn (ES2022, easy to miss)

Quick mention because people still write the verbose form:

js
// Old
Object.prototype.hasOwnProperty.call(obj, "key")

// New
Object.hasOwn(obj, "key")

The cheat sheet

  • Group an array: Object.groupBy
  • Promise resolve outside constructor: Promise.withResolvers
  • Set math: .union, .intersection, .difference
  • Lazy chains: iterator helpers
  • Cancel many ways: AbortSignal.any, AbortSignal.timeout
  • Deep copy: structuredClone
  • Escape regex input: RegExp.escape
  • Sync-or-async kickoff: Promise.try

Try them in one playground

// Object.groupBy
const txns = [
  { id: 1, type: "debit",  amount:  10 },
  { id: 2, type: "credit", amount:  20 },
  { id: 3, type: "debit",  amount:  30 },
];
console.log("groupBy:", Object.groupBy(txns, t => t.type));

// Set methods
const a = new Set([1, 2, 3, 4]);
const b = new Set([3, 4, 5, 6]);
console.log("union:",        [...a.union(b)]);
console.log("intersection:", [...a.intersection(b)]);
console.log("difference:",   [...a.difference(b)]);

// Promise.withResolvers
const { promise, resolve } = Promise.withResolvers();
setTimeout(() => resolve("hi from later"), 200);
console.log("withResolvers:", await promise);

// Iterator helpers
function* nat() { let n = 1; while (true) yield n++; }
console.log("first 5 evens:",
  nat().filter(n => n % 2 === 0).take(5).toArray()
);

// structuredClone
const og = { date: new Date(), nested: { n: 1 } };
const copy = structuredClone(og);
copy.nested.n = 99;
console.log("og still:", og.nested.n);

// AbortSignal.any
const c = new AbortController();
const signal = AbortSignal.any([c.signal, AbortSignal.timeout(50)]);
signal.addEventListener("abort", () => console.log("aborted:", signal.reason?.name));

// RegExp.escape (if available)
if (RegExp.escape) {
  const raw = "1 + 2";
  const re = new RegExp(RegExp.escape(raw));
  console.log("matches:", re.test("calc: 1 + 2 = 3"));
}

Quiz

Quiz1 / 3

Which built-in deep-clones an object including Dates and Maps?

Recap

  • Object.groupBy, Promise.withResolvers and the new Set methods delete code you used to write.
  • Iterator helpers turn generators into lazy data pipelines.
  • structuredClone is the right default for deep copies of data.
  • AbortSignal.any and AbortSignal.timeout make cancellation composable.
  • Temporal replaces Date. It ships in Firefox, Chrome, and Node 26, but not Safari, so browser code still needs the polyfill.
Built with Next.js, Tailwind & Sandpack.
Learn. Build. Ship.