JavaScript — What’s new with ECMAScript® 2024 (ES15) — In Depth Guide

Igor Komolov
9 min readNov 24, 2023

--

Live Article — Last Updated: February 07, 2024.

Discovering new functionality in programming languages is like the holidays or a birthday — it’s an exciting time filled with anticipation and the joy of exploring new gifts. With the proposed features of ES2024®, developers are on the brink of unwrapping a variety of enhancements that promise to make coding in JavaScript more efficient, readable, and robust. From the intuitive syntax of top-level await to the expressive power of the pipeline operator, and the reliability of immutable records and tuples, each new feature is like a carefully chosen present, designed to enrich the JavaScript ecosystem and empower developers with more tools at their disposal.

According to the ECMAScript 2024 Internationalization API Specification (ECMA-402 11th Edition) several features are slated for inclusion in ES2024.

Just keep in mind that some of these are still “proposals” so things will probably adapt a bit, but lucky for you, this article will adapt with the approved changes!

Without further ado…

Well-formed Unicode Strings

This feature aims to improve how JavaScript handles Unicode strings. Unicode strings are essential for representing a wide range of characters from different languages and symbols. This update would ensure consistent and accurate processing of these strings across different JavaScript environments.

const sampleStrings = [
// Examples with lone surrogates
"igor\uD800", // Leading surrogate
"igor\uD800komolov", // Leading surrogate followed by text
"\uDC00yourfuse", // Trailing surrogate
"your\uDC00fuse", // Trailing surrogate followed by text

// Well-formed examples
"yourFuse", // Regular string without surrogates
"emoji\uD83D\uDE00", // String with a complete surrogate pair (emoji)
];

sampleStrings.forEach(str => {
console.log(`Processed String: ${str.toWellFormed()}`);
});

// Expected output:
// "Processed String: igor�"
// "Processed String: igor�komolov"
// "Processed String: �yourfuse"
// "Processed String: your�fuse"
// "Processed String: yourFuse"
// "Processed String: emoji😀"

In the example above, the toWellFormed() method is applied to an array of strings, including some with lone surrogates and some well-formed strings. The method converts strings with lone surrogates into well-formed Unicode strings by replacing invalid sequences with a replacement character, while leaving already well-formed strings unchanged.

const problematicURL = "https://yourfuse.com/query=\uDC00data";

try {
encodeURI(problematicURL);
} catch (e) {
console.log('Error:', e.message); // Expected: URIError: URI malformed
}

// Using toWellFormed() to prevent the error
console.log('Well Formed URI:', encodeURI(problematicURL.toWellFormed()));
// Expected output: "https://yourfuse.com/query=%EF%BF%BDdata"
  • The variable problematicURL contains a URL with a lone trailing surrogate (\uDC00).
  • Attempting to encode this URL with encodeURI() throws a URIError due to the malformed Unicode string.
  • By applying toWellFormed(), the lone surrogate is replaced with the Unicode replacement character (U+FFFD, encoded as %EF%BF%BD), allowing encodeURI() to process it without errors.

Atomic waitSync

This addition targets concurrent operations, particularly in shared memory contexts. It provides a synchronization mechanism that’s vital for ensuring data integrity and preventing race conditions in multi-threaded operations. For example, waitSync can be used to synchronize access to a shared buffer between multiple workers.

Since we dont have an example yet as the docs are still being fleshed out, I can’t show you how it will be implement . However, we can take a guess based on the existing Atomics methods. Here goes…

// Assuming sharedArray is a SharedArrayBuffer
const sharedArray = new Int32Array(new SharedArrayBuffer(1024));

function performSynchronizedOperation(index, value) {
// The waitSync method would block execution until a certain condition is met.
// For example, it could wait until the value at the specified index is no longer equal to 0.
Atomics.waitSync(sharedArray, index, 0);

// Perform operations on shared memory
sharedArray[index] = value;

// Notify other threads or workers that the value at index has been updated
Atomics.notify(sharedArray, index, 1);
}

// In a web worker or another thread
performSynchronizedOperation(0, 123);

RegExp v flag with set notation + properties of strings

This improvement to regular expressions in JavaScript allows for more complex pattern matching and string manipulations. The ‘v’ flag and set notation enable more precise and expressive regex patterns. For instance, you could use this feature to match a set of characters with specific Unicode properties.

// difference/subtraction
[A--B]

// intersection
[A&&B]

// nested character class
[A--[0-9]]

A and B can be thought of as placeholders for a character class (e.g. [a-z]) or a property escape. Illustrative examples and FAQ for the proposal.

Top-level await

This “Just Do It” feature allows the await keyword to be used outside of async functions, making asynchronous code easier to write and read. For example, you can directly await a promise at the top level of a module, streamlining the code for importing modules or fetching data asynchronously.

// With top-level await
const data = await fetchData();
console.log(data);

Sure does breath some fresh air into heavy async/await structures!

Pipeline Operator

The pipeline operator (|>) improves the readability of code with multiple function calls. It allows for a functional-style syntax where the result of an expression is passed as an argument to the next function. For instance, you can refactor nested function calls into a clear sequence of operations:

// Without pipeline operator
const calculatedValue = Math.ceil(Math.pow(Math.max(0, -10), 1/3));

// With pipeline operator
const calculatedValue = -10
|> (n => Math.max(0, n)) // Replacing Math.max
|> (n => Math.pow(n, 1/3)) // Replacing Math.pow
|> Math.ceil; // Using Math.ceil

In this example:

  • The Math.max function ensures the number is not negative.
  • The Math.pow function calculates the cube root (raised to the power of 1/3).
  • The Math.ceil function rounds the number up to the nearest integer.

The pipeline operator (|>) simplifies chaining these operations, making the code more readable.

Now, the following is an example demonstrating the usefulness of the pipeline operator for data transformations:

// The pipeline operator simplifies complex data manipulations by allowing a series of functions to be applied in a clear, concise manner.

const numbers = [10, 20, 30, 40, 50];

const processedNumbers = numbers
|> (_ => _.map(n => n / 2)) // Halving each number
|> (_ => _.filter(n => n > 10)); // Filtering out numbers less than or equal to 10

console.log(processedNumbers); // [15, 20, 25]

In this example:

  • The map function halves each number in the array.
  • The filter function then removes any numbers that are 10 or less.
  • The pipeline operator (|>) elegantly chains these transformations, enhancing code readability.

Remember, the pipeline operator is still in “Draft”, Stage 2 of TC39.

Records and Tuples

These immutable data structures are similar to objects and arrays, respectively, but cannot be modified after creation. For example, updating a record or tuple results in a new instance:

// Creating an immutable record
const userProfile = #{
username: "IgorKomolov",
age: 39,
};

// Creating an immutable tuple
const numberSequence = #[10, 20, 30];

// Updating these structures creates new instances
const updatedProfile = userProfile.with({ age: 40});
console.log(updatedProfile); // #{ username: "IgorKomolov", age: 40 }
console.log(userProfile); // #{ username: "IgorKomolov", age: 39 } (remains the same)

const newNumberSequence = numberSequence.with(1, 25);
console.log(newNumberSequence); // #[10, 25, 30]
console.log(numberSequence); // #[10, 20, 30] (remains the same)

Records would function similarly to objects, and tuples would be akin to arrays. However, their defining feature is immutability.

Records and tuples can enhance performance in certain situations and enforce immutability in a codebase. While they are in Stage 2 of proposal and not yet implemented in JavaScript engines, developers can experiment with them using transpilers like Babel.

Decorators

Thank TypeScript for this one, it has been long awaited, and now out of the box! These allow for modifying or augmenting the behavior of classes, methods, properties, or parameters. They are particularly useful for adding metadata, logging, or modifying behaviors in a declarative manner:

// Applying a decorator to track the execution of a method
class SampleClass {
@trackExecution
performAction(parameter1, parameter2) {
// Method implementation goes here
}
}

In this example:

  • SampleClass is the class being defined.
  • @trackExecution is the decorator used to log or track the calls to the performAction method.
  • performAction is the method within SampleClass that takes two parameters (parameter1 and parameter2). The decorator will log or track every call made to this method.

Pattern Matching

This feature introduces a concise syntax for destructuring and matching complex data structures, enhancing code readability and reducing boilerplate.

(Researching) More coming soon, please check back later!

Temporal

Although it has been drafted for a while, the updated Temporal is a modern and comprehensive date-time API proposed for JavaScript and currently in Stage 3, designed to address many of the limitations and complexities of the existing Date object. Here are some examples of using Temporal in ES2024:

This object offers several factory methods to create Temporal values for the current time.

Getting the current instant in UTC

Temporal.Now.instant().toString()

Getting the current zoned date-time in a specific timezone

Temporal.Now.zonedDateTimeISO('Asia/Shanghai').toString()

Getting the current plain date-time in ISO format

Temporal.Now.plainDateTimeISO().toString()

Getting the current plain time in ISO format

Temporal.Now.plainTimeISO().toString()​​.

Properties of ZonedDateTime.prototype

The ZonedDateTime class in Temporal has several properties and methods that allow detailed manipulation and retrieval of date-time information.

  • These include getters for calendar, timezone, year, month, day, hour, minute, second, and even nanoseconds.
  • It also includes methods like .with(), .add(), .subtract(), .until(), .since(), and .round(), providing extensive functionality for working with zoned date-time values​​.

Plain Time Classes in Temporal

Temporal introduces “plain” classes, which are abstract representations of time without a time zone.

  • These classes include PlainDateTime, PlainDate, and PlainTime.
  • They are useful for displaying wall-clock time in a given time zone or for time computations where the time zone is irrelevant, like finding the first Tuesday of June 1984.

These examples demonstrate how Temporal in ES2024 can simplify and enhance date-time handling in JavaScript, providing more robust and versatile tools for developers.

Want to use it now? No problem!

Import the proposal or use a Babel Polyfil, heres how to import the proposal…

//Thats right you can import proposals :) 
import { Temporal } from '@std/proposal-temporal';


//Basic Ops
const now = Temporal.Now.zonedDateTimeISO('America/New_York');
console.log(now.toString());

//Manipulation & COmparison

const date = Temporal.PlainDate.from('2024-01-01');
const newDate = date.add({ days: 10 });
console.log(newDate.toString()); // Outputs '2024-01-11'

Ergonomic Brand Checks

Simplifies checking an object’s type in custom classes and data structures, making type verification more intuitive and less error-prone. See you later boilerplate!

Traditional Method (Before ES2024)

class Book {
#author;

constructor(author) {
this.#author = author;
}

static hasAuthorField(obj) {
try {
obj.#author; // Attempt to access the private field
return true; // Access successful
} catch (err) {
if (err instanceof TypeError) {
return false; // Access failed, field does not exist
}
throw err; // Other errors are re-thrown
}
}
}

// Example usage:
const myBook = new Book("Igor Komolov");
console.log(Book.hasAuthorField(myBook)); // Expected output: true

const otherObject = {};
console.log(Book.hasAuthorField(otherObject)); // Expected output: false

New ES2024 Method

class BookES2024 {
#author;

constructor(author) {
this.#author = author;
}

static hasAuthorField(obj) {
return #author in obj; // New ES2024 syntax for checking private field
}
}

// Example usage:
const myBook2024 = new BookES2024("Igor Komolov");
console.log(BookES2024.hasAuthorField(myBook2024)); // Expected output: true

const otherObject2024 = {};
console.log(BookES2024.hasAuthorField(otherObject2024)); // Expected output: false

In this example, the class Book demonstrates the traditional method, while BookES2024 uses the new ES2024 syntax. The hasAuthorField static method checks for the presence of the private field #author in an object, using different approaches in each class.

Realms API

This API offers a mechanism for creating isolated JavaScript environments. It’s useful for secure code execution and sandboxing, allowing you to run code in a controlled and isolated context. Plus, the name sounds so awesome!

Creating a Realm and Evaluating Simple Expressions

const igorsRealm = new Realm();
igorsRealm.evaluate('3 * 5'); // Evaluates to 15 in Igor's realm

Sharing Symbols Between Realms

const igorsRealm = new Realm();
Symbol.for('y') === igorsRealm.evaluate('Symbol.for("y")'); // returns true, shared symbol 'y'

Using Auto-Wrapped Functions

When a callable object is sent from one realm to another, a Wrapped Function Exotic Object is created in the target realm. This wrapped function, when called, chains the call to its connected function in the original realm.

const igorsRealm = new Realm();
const doubleFunction = igorsRealm.evaluate('num => num * 2');
doubleFunction(10); // returns 20

Function Evaluation with Callbacks

const igorsRealm = new Realm();
const processNumber = igorsRealm.evaluate('(number, callback) => callback(number + 5)');
processNumber(5, (result => console.log(result))); // Logs 10 (5 + 5)

Restricted Global Context Access

Accessing global objects like globalThis, arrays, or Object.prototype directly through realm.evaluate throws a TypeError.

const igorsRealm = new Realm();
igorsRealm.evaluate('this'); // Throws a TypeError
igorsRealm.evaluate('new Array()'); // Throws a TypeError
igorsRealm.evaluate('Object.keys({})'); // Throws a TypeError

The upcoming features in ES2024 are set to revolutionize the way we approach JavaScript coding. These enhancements not only promise to improve code readability and efficiency but also introduce powerful new paradigms like immutable data structures and advanced pattern matching. As these features move from proposal to implementation, they open up new possibilities for developers to write cleaner, more maintainable, and more expressive JavaScript code. The future of JavaScript looks bright with these advancements, signaling a continued evolution of a language that has become a cornerstone of modern web development.

--

--

Igor Komolov

Founder & CTO who writes code, day trades, cycles, golfs, takes pictures, makes art and reads ALOT.