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. 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, ES2025, and what's landing in ES2026, so you stop polyfilling things that are already built in.

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 (ES2026, broadly available)

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 (ES2026)

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
structuredClonedoesn't clone functions, DOM nodes, or class instances (those become plain objects). For normal data shapes, it's the answer.

Temporal (in preview)

Date is universally hated. Temporal is its replacement: timezone-aware, immutable, separates wall time from instants. Behind a polyfill today, becoming standard in browsers.

js
// Future / polyfilled syntax - the shape of things to come
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 is coming for Date. Start learning the shape now via the polyfill.
Built with Next.js, Tailwind & Sandpack.
Learn. Build. Ship.