ES2024 Features
Object.groupBy(), Map.groupBy(), Promise.withResolvers(), String.isWellFormed(), String.toWellFormed(), RegExp v flag (Unicode sets), resizable ArrayBuffers, ArrayBuffer.transfer(), Atomics.waitAsync().
Note: Array.fromAsync() is often mislabeled as ES2024 — it reached Stage 4 in 2025 and is part of ES2026 (see ES2026.md), even though runtimes have shipped it since 2023-24.
Contents
- Object.groupBy and Map.groupBy
- Promise.withResolvers
- Well-formed Unicode strings
- RegExp v flag
- Resizable ArrayBuffers and transfer
- Atomics.waitAsync
- Browser and Node.js support
Object.groupBy() and Map.groupBy()
Group array (or any iterable) elements by a key. These are static methods on Object and Map — there is no Array.prototype.group(); that earlier form of the proposal never shipped.
const inventory = [
{ name: 'asparagus', type: 'vegetable', quantity: 5 },
{ name: 'banana', type: 'fruit', quantity: 0 },
{ name: 'goat', type: 'meat', quantity: 23 },
{ name: 'cherry', type: 'fruit', quantity: 5 },
{ name: 'fish', type: 'meat', quantity: 22 }
];
// Group by type
const byType = Object.groupBy(inventory, item => item.type);
/*
{
vegetable: [{ name: 'asparagus', ... }],
fruit: [{ name: 'banana', ... }, { name: 'cherry', ... }],
meat: [{ name: 'goat', ... }, { name: 'fish', ... }]
}
*/
// Group by availability
const byAvailability = Object.groupBy(inventory, item =>
item.quantity > 0 ? 'inStock' : 'outOfStock'
);
// Map.groupBy returns a Map instead of a plain object
const byTypeMap = Map.groupBy(inventory, item => item.type);
byTypeMap.get('fruit'); // [{ name: 'banana', ... }, { name: 'cherry', ... }]Gotcha — the return value is a null-prototype object. Object.groupBy returns an object created with Object.create(null). Property access, spread, and destructuring all work, but inherited Object.prototype methods do not exist on it:
const groups = Object.groupBy([1, 2, 3], n => n % 2 ? 'odd' : 'even');
groups.odd; // [1, 3] ✓
Object.hasOwn(groups, 'odd'); // true ✓
// groups.hasOwnProperty('odd'); // TypeError: not a function
// `${groups}`; // TypeError: cannot convert to primitiveMigration from manual grouping:
// ❌ Before
const grouped = items.reduce((acc, item) => {
const key = item.category;
if (!acc[key]) acc[key] = [];
acc[key].push(item);
return acc;
}, {});// ✅ After
const grouped = Object.groupBy(items, item => item.category);When to use Map.groupBy:
// Use Map.groupBy when keys are non-strings. Object.groupBy coerces
// every key to a property key (string or symbol); Map keeps identity.
const byDate = Map.groupBy(events, event => event.date);
// Keys are Date objects — but note Map compares object keys by
// identity, so use a primitive (e.g. an ISO date string) when two equal
// dates should land in the same group.
// Map is also the right target when you'll iterate groups in order
const byPriority = Map.groupBy(tasks, task => task.priority);Promise.withResolvers()
Create a Promise with externally accessible resolve/reject.
// ❌ Before: Awkward closure pattern
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
// Use resolve/reject somewhere else
resolve('done');// ✅ After: Clean destructuring
const { promise, resolve, reject } = Promise.withResolvers();
// Use anywhere
setTimeout(() => resolve('done'), 1000);
await promise; // 'done'Practical example: Event-to-Promise:
function waitForEvent(element, eventName) {
const { promise, resolve } = Promise.withResolvers();
element.addEventListener(eventName, resolve, { once: true });
return promise;
}
const button = document.querySelector('button');
await waitForEvent(button, 'click');
console.log('Button was clicked!');Practical example: Manual async control:
class AsyncQueue {
#queue = [];
#waiters = [];
async next() {
if (this.#queue.length > 0) {
return this.#queue.shift();
}
const waiter = Promise.withResolvers();
this.#waiters.push(waiter.resolve);
return waiter.promise;
}
push(item) {
const resolve = this.#waiters.shift();
if (resolve) {
resolve(item);
} else {
this.#queue.push(item);
}
}
}Well-Formed Unicode Strings
Check and fix malformed UTF-16 strings.
// Check if string is well-formed
const valid = 'Hello 👋';
const invalid = 'Hi \uD800'; // Lone surrogate
valid.isWellFormed(); // true
invalid.isWellFormed(); // false
// Fix malformed strings
invalid.toWellFormed(); // 'Hi �' (replacement character)
// Use before encoding for URLs/APIs — encodeURIComponent throws on lone surrogates
function safeEncode(str) {
return encodeURIComponent(str.toWellFormed());
}RegExp v Flag (Unicode Sets)
Enhanced Unicode handling in regex: set operations in character classes, string properties, nested classes. /v is a superset of /u — the two flags cannot be combined.
// Set operations in character classes
const emoji = /[\p{Emoji}--\p{ASCII}]/v; // Emoji minus ASCII
// String properties (multi-code-point matches, /v only)
const flags = /\p{RGI_Emoji}/v; // Matches full emoji sequences like 🇺🇸
// Intersection
const pattern = /[[a-z]&&[^aeiou]]/v; // Consonants only
// Nested character classes
const complex = /[[0-9]--[0-4]]/v; // 5-9 onlyResizable ArrayBuffers and transfer()
// Create resizable buffer
const buffer = new ArrayBuffer(1024, { maxByteLength: 4096 });
console.log(buffer.byteLength); // 1024
console.log(buffer.maxByteLength); // 4096
console.log(buffer.resizable); // true
// Resize within bounds
buffer.resize(2048);
console.log(buffer.byteLength); // 2048
// Transfer ownership and detach the source buffer
const newBuffer = buffer.transfer(512);
console.log(buffer.detached); // true
console.log(newBuffer.byteLength); // 512Atomics.waitAsync()
Asynchronous shared memory waiting.
const sharedBuffer = new SharedArrayBuffer(4);
const sharedArray = new Int32Array(sharedBuffer);
// Async wait (doesn't block main thread)
const result = Atomics.waitAsync(sharedArray, 0, 0);
const status = result.async ? await result.value : result.value;
console.log('Wait result:', status); // 'ok', 'timed-out', or 'not-equal'
// In another context/worker
Atomics.notify(sharedArray, 0);Browser/Node.js Support
First versions with unflagged support (source: MDN browser-compat-data, 2026):
| Feature | Chrome | Firefox | Safari | Node.js |
|---|---|---|---|---|
| Object.groupBy / Map.groupBy | 117+ | 119+ | 17.4+ | 21+ |
| Promise.withResolvers | 119+ | 121+ | 17.4+ | 22+ |
| String.isWellFormed/toWellFormed | 111+ | 119+ | 16.4+ | 20+ |
| RegExp v flag | 112+ | 116+ | 17+ | 20+ |
| Resizable ArrayBuffer | 111+ | 128+ | 16.4+ | 20+ |
| ArrayBuffer.transfer | 114+ | 122+ | 17.4+ | 21+ |
| Atomics.waitAsync | 90+ | 145+ | 16.4+ | 16+ |
All of these are in Node 22 and current evergreen browsers, but older WebViews and embedded runtimes may lag. Re-check MDN Browser Compatibility Data against the project's targets.