DEV Community

Cover image for ES2024 Features You'll Actually Use in JavaScript
Umesh Malik
Umesh Malik

Posted on Edited on Originally published at umesh-malik.com

ES2024 Features You'll Actually Use in JavaScript

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.groupBy kill the hand-rolled reduce grouping utility every codebase has one of.
  • Promise.withResolvers() gives you resolve/reject outside the executor — cleaner event-driven and timeout code.
  • String.isWellFormed() / toWellFormed() catch lone surrogates before they break encodeURIComponent or a search index.
  • The RegExp v flag 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', ... }]
// }
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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;
});
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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

Key Takeaways

  • Object.groupBy eliminates one of the most common utility functions in JavaScript projects
  • Promise.withResolvers cleans up the deferred promise pattern
  • Well-formed Unicode methods prevent silent encoding failures
  • The RegExp v flag 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)