ECMAScript 2024: pragmatic evolution

The ECMAScript 2024 specification (ES15) was officially approved in June 2024 and brings features that many developers had been requesting for years. Unlike previous versions that focused on syntax, ES2024 focuses on practical utilities that simplify day-to-day code.

Object.groupBy() and Map.groupBy()

Without a doubt, the star of ES2024. Object.groupBy() lets you group elements of an iterable using a classifier function.

Before vs. Now

// BEFORE: manual grouping with reduce
const porCategoria = productos.reduce((acc, producto) => {
  const key = producto.categoria;
  if (!acc[key]) acc[key] = [];
  acc[key].push(producto);
  return acc;
}, {});

// NOW: a single line
const porCategoria = Object.groupBy(productos, p => p.categoria);

Map.groupBy() for non-string keys

When you need keys that are not strings (objects, numbers, etc.), use Map.groupBy():

const usuarios = [
  { nombre: 'Ana', edad: 25 },
  { nombre: 'Carlos', edad: 17 },
  { nombre: 'Diana', edad: 30 },
  { nombre: 'Luis', edad: 15 },
];

const porMayoriaEdad = Map.groupBy(usuarios, u =>
  u.edad >= 18 ? 'adulto' : 'menor'
);

// Map { 'adulto' => [...], 'menor' => [...] }

Important considerations

  • Object.groupBy() creates an object without a prototype (Object.create(null))
  • Keys are converted to strings (as in regular objects)
  • For complex keys, use Map.groupBy()
  • The original iterable is not modified

Promise.withResolvers()

This new static method simplifies a very common pattern: creating a Promise and extracting its resolve and reject functions.

// BEFORE: common but verbose pattern
let resolve, reject;
const promise = new Promise((res, rej) => {
  resolve = res;
  reject = rej;
});

// NOW: clean and direct
const { promise, resolve, reject } = Promise.withResolvers();

Real-world use case: wrapping an EventEmitter

function esperarEvento(emitter, evento) {
  const { promise, resolve, reject } = Promise.withResolvers();

  emitter.addEventListener(evento, resolve, { once: true });
  emitter.addEventListener('error', reject, { once: true });

  return promise;
}

// Usage
const resultado = await esperarEvento(socket, 'message');

Resizable and transferable ArrayBuffer

ES2024 introduces ArrayBuffer with adjustable size and the ability to transfer ownership between contexts.

Resizable ArrayBuffer

// Create a buffer that can grow up to 1MB
const buffer = new ArrayBuffer(256, { maxByteLength: 1024 * 1024 });

console.log(buffer.byteLength); // 256
console.log(buffer.maxByteLength); // 1048576
console.log(buffer.resizable); // true

// Resize
buffer.resize(512);
console.log(buffer.byteLength); // 512

ArrayBuffer transfer

const original = new ArrayBuffer(1024);
const view = new Uint8Array(original);
view[0] = 42;

// Transfer to a new buffer
const transferido = original.transfer();

console.log(original.byteLength); // 0 (detached)
console.log(transferido.byteLength); // 1024
console.log(new Uint8Array(transferido)[0]); // 42

This is especially useful when communicating with Web Workers, eliminating the need to copy data.

String.isWellFormed() and String.toWellFormed()

JavaScript allows strings with incomplete surrogate pairs, which can cause problems when interacting with APIs like encodeURIComponent(). These new methods solve that.

const texto = 'Hola \uD800 mundo'; // lone surrogate

console.log(texto.isWellFormed()); // false

const textoCorregido = texto.toWellFormed();
console.log(textoCorregido.isWellFormed()); // true
// Replaces lone surrogates with U+FFFD (replacement character)

When to use them?

  • Before sending strings to external APIs
  • When processing data from untrusted sources
  • When using TextEncoder or encodeURIComponent()

Atomics.waitAsync()

An asynchronous version of Atomics.wait() that does not block the main thread:

const sharedBuffer = new SharedArrayBuffer(4);
const sharedArray = new Int32Array(sharedBuffer);

// Does not block the main thread
const resultado = Atomics.waitAsync(sharedArray, 0, 0);

if (resultado.async) {
  resultado.value.then(status => {
    console.log('Worker notificado:', status);
  });
}

// In another Worker:
Atomics.store(sharedArray, 0, 1);
Atomics.notify(sharedArray, 0);

RegExp v flag (unicodeSets)

The new v flag extends regular expression capabilities with Unicode character sets:

// Search for emojis (with v flag)
const emojiRegex = /\p{RGI_Emoji}/v;
console.log(emojiRegex.test('hola')); // false

// Set intersection: Greek letters that are uppercase
const regex = /[\p{Script=Greek}&&\p{Lu}]/v;
console.log(regex.test('A')); // false (Latin)

// Set difference: letters except vowels
const consonantes = /[\p{Letter}--[aeiouAEIOU]]/v;

Set operations with the v flag

Operation Syntax Description
Union [AB] A or B
Intersection [A&&B] A and B
Difference [A--B] A but not B

Browser support summary

Feature Chrome Firefox Safari Node.js
Object.groupBy 117+ 119+ 17.4+ 21+
Promise.withResolvers 119+ 121+ 17.4+ 22+
ArrayBuffer.transfer 114+ 122+ 17.4+ 22+
String.isWellFormed 111+ 119+ 16.4+ 20+
Atomics.waitAsync 87+ 128+ 16.4+ 16+
RegExp v flag 112+ 116+ 17+ 20+

Conclusion

ES2024 brings improvements that, while not as flashy as arrow functions or async/await, solve real day-to-day development friction. Object.groupBy() and Promise.withResolvers() in particular will change the way we write JavaScript. The best part: they already have broad support across all modern browsers. It is time to adopt them.