JavaScript's Evolution Continues

Mikey NicholsMikey Nichols
10 min read

The Maturing of Modern JavaScript

JavaScript's evolution continues at a steady pace, with each annual edition introducing features that enhance developer productivity and code quality. ES13 (ECMAScript 2022) and ES14 (ECMAScript 2023) represent significant milestones in this journey, bringing both syntax improvements and functional enhancements that make JavaScript more powerful, safer, and more expressive with the ES13 edition published in June 2022, followed by ES14 in June 2023.

What makes these recent updates particularly interesting is how they address real-world developer pain points, from better class handling to more functional approaches to array manipulation. Let's dive into the standout features of both editions and see how they're transforming modern JavaScript development.

ES13 (ECMAScript 2022): Making Classes More Powerful

Class Fields: Public, Private, and Static

One of the most substantial improvements in ES13 is the enhanced class system. JavaScript's class syntax, introduced in ES6, has been gradually enhanced over the years, but ES13 brings it closer to the object-oriented model familiar to developers from other languages.

Before ES13, class fields had to be declared inside constructors, which often made classes verbose and less readable:

class User {
  constructor() {
    this.name = "Alice";  // Public field
    this._email = "alice@example.com";  // Convention for "private" (not actually private)
  }
}

With ES13, we can now declare fields directly in the class body, making code more concise and intuitive without having to define them inside of the constructor:

class User {
  name = "Alice";  // Public field
  #email = "alice@example.com";  // Truly private field
}

The # prefix creates genuinely private fields that cannot be accessed from outside the class. This is a major improvement over the underscore convention, which was merely a signal to other developers but didn't prevent external access an actual error will be thrown if we try to access or modify the field outside of the class.

ES13 also introduces static class fields and methods that belong to the class itself rather than to instances:

class User {
  name = "Alice";
  #email = "alice@example.com";

  static userCount = 0;

  constructor() {
    User.userCount++;
  }

  static hasPrivateEmail(obj) {
    return #email in obj;  // Check if private field exists
  }
}

console.log(User.userCount);  // 0
const user = new User();
console.log(User.userCount);  // 1
console.log(User.hasPrivateEmail(user));  // true

The in operator now works with private fields, allowing developers to check if an object has a specific private field we can simply use the in operator to check whether a private field or method exists in a class or not.

Top-Level Await

Before ES13, the await keyword could only be used inside an async function. This often led to unnecessary wrapper functions just to handle Promise resolution:

// Before ES13
const getData = async () => {
  const response = await fetch('https://api.example.com/data');
  return response.json();
};

getData().then(data => {
  console.log(data);
});

ES13 introduces top-level await, which means you can use await directly in modules without wrapping it in an async function the await keyword can also be used outside the async functions at the top level of a module to wait for a Promise:

// With ES13
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);

This feature is particularly useful for dynamic imports, dependency fallbacks, and resource initialization:

// Dynamically import a module
let module;
try {
  module = await import('./primary-module.js');
} catch {
  // Fallback if primary module fails to load
  module = await import('./fallback-module.js');
}

The at() Method

ES13 introduces the .at() method for arrays, strings, and typed arrays, which offers a more intuitive way to access elements, especially with negative indices we can access the elements of an array or a string backwards using the at method with negative indexing:

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

// Traditional way to get the last element
const lastElement = array[array.length - 1];  // 50

// With ES13 at() method
const lastElement = array.at(-1);  // 50
const secondToLast = array.at(-2);  // 40

This makes code more readable when accessing elements from the end of a collection.

Regular Expression Match Indices

ES13 enhances regular expressions with a new /d flag that provides start and end indices for capture groups Regular expression match indices via the /d flag, which provides start and end indices for matched substrings:

const regex = /(a)(b)/d;
const result = regex.exec('ab');
console.log(result.indices);  // [[0, 2], [0, 1], [1, 2]]
// Overall match: [0, 2]
// First group (a): [0, 1]
// Second group (b): [1, 2]

This feature is particularly useful for syntax highlighting, code analysis tools, and other applications where the position of matches is important.

Error Cause

Error handling gets improved with the addition of the cause property, which allows errors to include information about what caused them The cause property on Error objects, that can be used to record a causation chain in errors:

try {
  // Some operation that might fail
  throw new Error('Failed to fetch data', { cause: 'Network disconnected' });
} catch (error) {
  console.log(error.message);  // 'Failed to fetch data'
  console.log(error.cause);    // 'Network disconnected'
}

This creates a more informative error chain, making debugging and error handling more effective.

ES14 (ECMAScript 2023): Working Smarter with Arrays

Array Methods That Don't Mutate

ES14 introduces a set of array methods that create new arrays instead of modifying the original. This functional programming approach helps avoid unwanted side effects and makes code more predictable This version introduces the toSorted, toReversed, with, findLast, and findLastIndex methods on Array.prototype and TypedArray.prototype, as well as the toSpliced method on Array.prototype.

toSorted()

The toSorted() method works like sort() but returns a new array instead of modifying the original toSorted has the same signature as Array.prototype.sort(), but it creates a new array instead of operating on the array itself:

const numbers = [3, 1, 4, 1, 5, 9];
const sorted = numbers.toSorted();

console.log(sorted);      // [1, 1, 3, 4, 5, 9]
console.log(numbers);     // [3, 1, 4, 1, 5, 9] - original unchanged

// With custom sorting
const descending = numbers.toSorted((a, b) => b - a);
console.log(descending);  // [9, 5, 4, 3, 1, 1]

toReversed()

Similarly, toReversed() is the non-mutating version of reverse() The toReversed() method returns a new array with the elements in reverse order.

const fruits = ['apple', 'banana', 'cherry'];
const reversed = fruits.toReversed();

console.log(reversed);    // ['cherry', 'banana', 'apple']
console.log(fruits);      // ['apple', 'banana', 'cherry'] - original unchanged

toSpliced()

The toSpliced() method provides a non-mutating way to add, remove, or replace elements ES14 added toSpliced method to an array.prototype.

const colors = ['red', 'green', 'blue'];
const newColors = colors.toSpliced(1, 1, 'yellow');

console.log(newColors);   // ['red', 'yellow', 'blue']
console.log(colors);      // ['red', 'green', 'blue'] - original unchanged

with()

The with() method creates a new array with one element replaced at a specific index The new with() method lets you modify a single element based on its index, and get back a new array.

const planets = ['Mercury', 'Venus', 'Earth', 'Mars'];
const corrected = planets.with(1, 'Venus (not habitable)');

console.log(corrected);   // ['Mercury', 'Venus (not habitable)', 'Earth', 'Mars']
console.log(planets);     // ['Mercury', 'Venus', 'Earth', 'Mars'] - original unchanged

JavaScript Arrays Evolved: Key ES10 & ES14 Methods and When to Use Them

Modern JavaScript has introduced several powerful array methods in ES10 (2019) and ES14 (2023) that enhance the language's functional programming capabilities while providing clearer syntax for common operations. ES10 brought Array.flat() and Array.flatMap(), which simplify working with nested arrays by flattening them (either to a specified depth or recursively) and combining mapping with flattening in a single step, respectively. These methods are non-mutating, meaning they return new arrays without modifying the original. In contrast, ES14 introduced Array.prototype.toReversed(), Array.prototype.toSorted(), Array.prototype.toSpliced(), and Array.prototype.with(), which provide non-mutating alternatives to traditional mutating methods like reverse(), sort(), splice(), and direct index assignment. This distinction is crucial—mutating methods alter the original array, which can lead to unintended side effects, while non-mutating operations ensure immutability, making code more predictable and easier to debug. These additions reflect JavaScript's shift toward immutable data patterns, particularly useful in state management frameworks like React and Redux. Understanding when to use mutating versus non-mutating methods helps developers write cleaner, more maintainable code while avoiding common pitfalls related to array manipulation.

Finding Elements from the End

ES14 adds findLast() and findLastIndex() methods to arrays, which work like their counterparts find() and findIndex() but search from the end of the array findLast traverses an array from the end and retrieves the value of the first element that satisfies the condition.

const numbers = [5, 12, 8, 130, 44, 4, 12];

// Find last number greater than 10
const lastBig = numbers.findLast(num => num > 10);
console.log(lastBig);  // 12 (the last 12 in the array)

// Find its index
const lastBigIndex = numbers.findLastIndex(num => num > 10);
console.log(lastBigIndex);  // 6

These methods are particularly useful when you need to find the most recent occurrence of something in an array.

Hashbang Grammar

ES14 adds formal support for hashbang comments (sometimes called shebangs) at the beginning of JavaScript files added support for #! shebang comments at the beginning of files to better facilitate executable ECMAScript files.

#!/usr/bin/env node

console.log('Hello, world!');

This makes it easier to write executable JavaScript scripts that can be run directly from the command line on Unix-like systems.

Symbol as WeakMap Keys

ES14 expands what can be used as keys in weak collections. Previously, only objects could be used as keys in WeakMap, WeakSet, etc. Now, symbols can also be used as keys in these collections symbols as WeakMap keys.

const weakMap = new WeakMap();
const key = Symbol('unique identifier');

weakMap.set(key, { data: 'Important information' });
console.log(weakMap.get(key));  // { data: 'Important information' }

This change allows for more flexible memory management patterns, particularly in libraries and frameworks.

The Impact on Modern JavaScript Development

These new features in ES13 and ES14 collectively represent JavaScript's continuing evolution toward a more mature, developer-friendly language. Several key themes emerge:

More Functional Programming Support

The non-mutating array methods in ES14 and other immutable patterns show a clear shift toward functional programming paradigms, making it easier to write code that avoids side effects. This approach is increasingly favored in modern JavaScript applications for its predictability and testability.

Better Object-Oriented Programming

The improvements to classes in ES13, particularly private fields and static members, bring JavaScript's object-oriented capabilities more in line with other class-based languages. This makes it easier for developers coming from languages like Java or C# to work effectively in JavaScript.

Enhanced Ergonomics

Features like at(), top-level await, and findLast() simply make JavaScript more pleasant to work with by reducing boilerplate and making common operations more intuitive.

Improved Error Handling

The cause property for errors improves debugging and error reporting, especially in complex applications where understanding the chain of failures is crucial.

From Theory to Practice: Modern JavaScript in Action

To truly appreciate the power of ES13 and ES14's new features, let's move beyond individual examples and see how they work together in a real-world application. The following Task Manager demo combines multiple modern JavaScript features into a cohesive, practical system that showcases their combined benefits. By implementing private class fields, non-mutating array methods, error causes, and more within a single application, we can better understand how these features complement each other and create more maintainable, readable code. This interactive demo not only illustrates the syntax of these new features but demonstrates their practical advantages in an everyday programming context. Whether you're managing data, handling user interactions, or organizing application logic, you'll see how ES13 and ES14 provide elegant solutions to common development challenges.

Adoption Considerations

While these features bring significant improvements, developers should be aware of browser and runtime compatibility. Most modern browsers now support ES13 features, but ES14 support is still growing. Tools like Babel can help transpile code with newer features for older environments when needed.

For production applications, consider these adoption strategies:

  1. Check browser compatibility before using these features natively

  2. Use transpilers like Babel for broader compatibility

  3. Consider polyfills for specific features in older environments

  4. Progressively adopt new features in non-critical code paths first

Conclusion: The JavaScript Evolution Continues

ES13 and ES14 demonstrate how JavaScript continues to evolve in response to developer needs and industry trends. While some languages change dramatically between versions, JavaScript's evolution has been more incremental, focusing on practical improvements that enhance developer experience without breaking compatibility.

As we look ahead to future ECMAScript editions, we can expect this pattern to continue—thoughtful additions that make JavaScript more powerful, more expressive, and more maintainable, while preserving its fundamental character as the language of the web.

These latest updates reinforce JavaScript's position as a versatile, ever-improving language that continues to adapt to the changing landscape of web and application development. Whether you're building front-end interfaces, back-end services, or full-stack applications, the features in ES13 and ES14 offer new tools to write better JavaScript code.

0
Subscribe to my newsletter

Read articles from Mikey Nichols directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Mikey Nichols
Mikey Nichols

I am an aspiring web developer on a mission to kick down the door into tech. Join me as I take the essential steps toward this goal and hopefully inspire others to do the same!