The ES2024 features below are the ones I've actually reached for in day-to-day frontend work — not spec trivia nobody ships. Here's a practical look at the ones worth adopting now. If you write TypeScript day to day, these pair well with my complete guide to TypeScript utility types. Related reading: React Performance Optimization and SvelteKit vs Next.js in 2026.
What Is ES2024?
ES2024 is the JavaScript language spec update — ECMAScript 2024, ratified June 2024 — that adds native array grouping, deferred promises, well-formed Unicode string checks, and Unicode set operations in regex.
TL;DR
-
Object.groupBy/Map.groupBykill the hand-rolledreducegrouping utility every codebase has one of. -
Promise.withResolvers()gives youresolve/rejectoutside the executor — cleaner event-driven and timeout code. -
String.isWellFormed()/toWellFormed()catch lone surrogates before they breakencodeURIComponentor a search index. - The RegExp
vflag adds set subtraction and intersection for real Unicode-aware pattern matching. - All four are shipped and stable in every major browser and Node.js 22+ — safe to use today, no polyfill needed.
What Are the Biggest ES2024 Features?
Five ES2024 features actually change how you write everyday code: Object.groupBy/Map.groupBy, Promise.withResolvers, well-formed Unicode string methods, the RegExp v flag, and ArrayBuffer.transfer() alongside Atomics.waitAsync() for worker-heavy apps. The rest of this post walks through each one with real code.
Object.groupBy and Map.groupBy
Grouping arrays by a property has been a common utility function in every project I've worked on. ES2024 makes it native.
Before ES2024 (manual reduce) |
ES2024 (Object.groupBy) |
|---|---|
Write and maintain a groupBy helper per project |
Zero-dependency, built into the language |
| Easy to get the accumulator initialization wrong | No accumulator to manage |
| Returns a plain mutable object either way | Returns a null-prototype object, safer against prototype pollution |
| No non-string-key variant without extra code |
Map.groupBy handles non-string keys natively |
Object.groupBy
const products = [
{ name: 'Laptop', category: 'electronics', price: 999 },
{ name: 'Shirt', category: 'clothing', price: 29 },
{ name: 'Phone', category: 'electronics', price: 699 },
{ name: 'Jeans', category: 'clothing', price: 59 },
{ name: 'Tablet', category: 'electronics', price: 449 },
];
const grouped = Object.groupBy(products, (product) => product.category);
// Result:
// {
// electronics: [{ name: 'Laptop', ... }, { name: 'Phone', ... }, { name: 'Tablet', ... }],
// clothing: [{ name: 'Shirt', ... }, { name: 'Jeans', ... }]
// }
This replaces the reduce boilerplate we've all written dozens of times. At Expedia, we had a utility called groupBy that did exactly this — now it's built in.
Map.groupBy
When you need non-string keys, use Map.groupBy:
const grouped = Map.groupBy(products, (product) =>
product.price > 500 ? 'premium' : 'budget'
);
grouped.get('premium'); // [Laptop, Phone]
grouped.get('budget'); // [Shirt, Jeans, Tablet]
Promise.withResolvers
This is one of those features that eliminates an awkward pattern. Previously, to get external access to resolve and reject, you had to do this:
// Before ES2024
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
Now it's clean:
// ES2024
const { promise, resolve, reject } = Promise.withResolvers();
// Use it in event-driven code
button.addEventListener('click', () => resolve('clicked'), { once: true });
const result = await promise;
This is particularly useful for wrapping callback-based APIs or building custom async coordination patterns.
Real-World Example: Timeout Wrapper
function withTimeout(asyncFn, ms) {
const { promise: timeoutPromise, reject } = Promise.withResolvers();
const timer = setTimeout(() => reject(new Error('Timeout')), ms);
return Promise.race([
asyncFn().finally(() => clearTimeout(timer)),
timeoutPromise,
]);
}
// Usage
const data = await withTimeout(() => fetch('/api/data'), 5000);
Well-Formed Unicode Strings
String.prototype.isWellFormed() and String.prototype.toWellFormed() help you deal with lone surrogates — characters that can cause issues in encodeURIComponent and other APIs.
const problematic = 'Hello \uD800 World';
problematic.isWellFormed(); // false
problematic.toWellFormed(); // 'Hello � World' (lone surrogate replaced)
// Safe encoding
const safeStr = input.isWellFormed() ? input : input.toWellFormed();
const encoded = encodeURIComponent(safeStr); // No more URIError
At Tekion, we dealt with user-generated content from dealership forms in multiple languages. Malformed Unicode caused silent failures in our search indexing pipeline. These methods would have caught those issues early — a validation guard before the string ever reaches encodeURIComponent or a downstream search index is a five-minute fix once you know the method exists.
The failure mode is nasty precisely because it's silent. A lone surrogate doesn't throw when you create the string — it throws (or worse, produces mojibake) three layers downstream, in a URL encoder, a JSON serializer, or a database driver that assumes valid UTF-16. isWellFormed() lets you check at the boundary, right where the untrusted input enters your system, instead of chasing the failure through a stack trace that has nothing to do with the real cause.
RegExp v Flag (Unicode Sets)
The new v flag replaces the u flag with extended capabilities for matching Unicode characters and set operations.
// Match any emoji
const emojiRegex = /\p{Emoji}/v;
emojiRegex.test('👋'); // true
// Set subtraction: match Greek letters except specific ones
const regex = /[\p{Script=Greek}--[αβγ]]/v;
regex.test('δ'); // true
regex.test('α'); // false
// Set intersection: match characters that are both ASCII and digits
const asciiDigits = /[\p{ASCII}&&\p{Number}]/v;
asciiDigits.test('5'); // true
asciiDigits.test('٥'); // false (Arabic-Indic digit)
ArrayBuffer Transfer
ArrayBuffer.prototype.transfer() lets you efficiently move ownership of a buffer's memory, similar to Rust's ownership model.
const buffer = new ArrayBuffer(1024);
const transferred = buffer.transfer();
buffer.byteLength; // 0 (original is now detached)
transferred.byteLength; // 1024
// Resize during transfer
const resized = buffer.transfer(2048);
This is useful in performance-critical scenarios like WebGL, audio processing, or working with large binary data in Web Workers. Before transfer(), moving a large buffer to a worker meant either copying it (expensive) or using postMessage's transferable-objects list (works, but ties you to the message-passing API). transfer() gives you the same zero-copy ownership move as a plain method call, so you can hand off memory inside regular application code, not just across a postMessage boundary.
Atomics.waitAsync
Atomics.waitAsync() provides non-blocking waiting on shared memory, enabling better coordination between the main thread and Web Workers.
const sharedBuffer = new SharedArrayBuffer(4);
const sharedArray = new Int32Array(sharedBuffer);
// Non-blocking wait on main thread
const result = Atomics.waitAsync(sharedArray, 0, 0);
result.value.then(() => {
console.log('Worker signaled completion');
});
// In worker: Atomics.notify(sharedArray, 0);
How Do I Choose Which ES2024 Feature to Adopt First?
These features have strong browser support as of early 2025. Here is my recommendation for adopting them: start with the ones that remove code you already maintain (Object.groupBy, Promise.withResolvers, well-formed Unicode checks), and treat the runtime-dependent ones (v flag, ArrayBuffer.transfer(), Atomics.waitAsync()) as opportunistic upgrades once your support matrix clears them.
FAQ
Sources
- Object.groupBy() — MDN Web Docs
- Promise.withResolvers() — MDN Web Docs
- String.prototype.isWellFormed() — MDN Web Docs
- ECMAScript regular expression v flag proposal — tc39.es
Key Takeaways
-
Object.groupByeliminates one of the most common utility functions in JavaScript projects -
Promise.withResolverscleans up the deferred promise pattern - Well-formed Unicode methods prevent silent encoding failures
- The RegExp
vflag enables powerful Unicode-aware pattern matching - These features are production-ready in modern browsers and Node.js 22+
Originally published at umesh-malik.com
Keep reading on umesh-malik.com:
Top comments (0)