A Summary of New Features in ES2022 (ES13)

This article covers the new functions/features introduced in ECMAScript 2022 (ES2022 or ES13). We’ll look at brief examples and describe the purpose of each function.

A link to the actual original proposal (on GitHub) is also added below each function so you can better understand it.

Regular expression matching index

Currently, only the matching start index is returned when using the Regex API in JavaScript. However, for some special advanced scenarios, this is not enough.

As part of this specification, a special flag d was added. Using it, the regular expression API will return a two-dimensional array as the keys of the name index. It contains the start and end index of each match.

If any named groups are captured in the regex, it will return their start/end indices in the indices.groups object, and the named group name will be its key.

// a regex with a 'B' named group capture
const expr = /a+(?<B>b+)+c/d;

const result = expr.exec("aaabbbc")

// shows start-end matches + named group match
console.log(result.indices);
// prints [Array(2), Array(2), groups: {…}]

// showing the named 'B' group match
console.log(result.indices.groups['B'])
// prints [3, 6]

https://github.com/tc39/proposal-regexp-match-indices

 

Top-level await

Before this proposal, top-level await was not accepted, but there were workarounds to emulate this behavior, with some drawbacks.

The top-level await feature lets us rely on modules to handle promises. This is an intuitive function.

Note, however, that it may change the execution order of modules. If a module depends on another module with a top-level await call, the execution of that module will pause until the promise completes.

Let’s see an example:

// users.js
export const users = await fetch('/users/lists');

// usage.js
import { users } from "./users.js";
// the module will wait for users to be fullfilled prior to executing any code
console.log(users);

In the above example, the engine will wait for the user to complete the action before executing the code on the usage.js module.

All in all, this is a nice and intuitive feature that needs to be used with care.

https://github.com/tc39/proposal-top-level-await

 

.at( )

JavaScript has long been asked to provide a Python-like negative index accessor for arrays. Instead of doing array[array.length-1] do simple array[-1]. This is impossible because the [] notation is also used for objects in JavaScript.

The accepted proposal took a more practical approach.

The Array object will now have a method to simulate the above behavior.

const array = [1,2,3,4,5,6]

// When used with positive index it is equal to [index]
array.at(0) // 1
array[0] // 1

// When used with negative index it mimicks the Python behaviour
array.at(-1) // 6
array.at(-2) // 5
array.at(-4) // 3

https://github.com/tc39/proposal-relative-indexing-method

By the way, since we’re talking about arrays, did you know that you can destructure array positions?

const array = [1,2,3,4,5,6];

// Different ways of accessing the third position
const {3: third} = array; // third = 4
array.at(3) // 4
array[3] // 4

Accessible Object.prototype.hasOwnProperty

The following function is just a simplification of the already pre-existing hasOwnProperty. However, it needs to be called in the lookup instance we want to perform.

So it’s not uncommon for many developers to end up doing this:

const x = { foo: "bar" };

// grabbing the hasOwnProperty function from prototype
const hasOwnProperty = Object.prototype.hasOwnProperty

// executing it with the x context
if (hasOwnProperty.call(x, "foo")) {
  ...
}

With these new specifications, a hasOwn method was added to the Object prototype, and now, we can simply do:

const x = { foo: "bar" };

// sing the new Object method
if (Object.hasOwn(x, "foo")) {
  ...
}

https://github.com/tc39/proposal-accessible-object-hasownproperty

 

Error Cause

Errors help us identify and react to unexpected behavior of our application; however, understanding the root cause of deeply nested errors, and handling them properly can become challenging; we lose stack traces when catching and rethrowing their information.

There is no explicit agreement on what to do, and considering any error handling, we have at least 3 options:

async function fetchUserPreferences() {
  try { 
    const users = await fetch('//user/preferences')
      .catch(err => {
        // What is the best way to wrap the error?
        // 1. throw new Error('Failed to fetch preferences ' + err.message);
        // 2. const wrapErr = new Error('Failed to fetch preferences');
        //    wrapErr.cause = err;
        //    throw wrapErr;
        // 3. class CustomError extends Error {
        //      constructor(msg, cause) {
        //        super(msg);
        //        this.cause = cause;
        //      }
        //    }
        //    throw new CustomError('Failed to fetch preferences', err);
      })
    }
}
fetchUserPreferences();

As part of this new specification, we can construct a new error and keep a reference to the obtained error.

We just pass the object {cause: err} to the Error constructor.

It all becomes more straightforward, standard, and easy to understand deeply nested errors; let’s look at an example:

async function fetcUserPreferences() {
  try { 
    const users = await fetch('//user/preferences')
      .catch(err => {
        throw new Error('Failed to fetch user preferences, {cause: err});
      })
    }
}
fetcUserPreferences();

https://github.com/tc39/proposal-error-cause

 

Class Fields

Before this release, there was no proper way to create a private field, there were some ways around it by using boost, but it wasn’t a proper private field.

With the new spec, we just need to add the # character to our variable declaration.

class Foo {
  #iteration = 0;

  increment() {
    this.#iteration++;
  }

  logIteration() {
    console.log(this.#iteration);
  }
}

const x = new Foo();

// Uncaught SyntaxError: Private field '#iteration' must be declared in an enclosing class
x.#iteration

// works
x.increment();

// works
x.logIteration();

Having private fields means we have strong encapsulation boundaries and cannot access class variables from the outside, which shows that the class keyword is no longer just sugar syntax.

We can also create private methods:

class Foo {
  #iteration = 0;

  #auditIncrement() {
    console.log('auditing');
  }

  increment() {
    this.#iteration++;
    this.#auditIncrement();
  }
}

const x = new Foo();

// Uncaught SyntaxError: Private field '#auditIncrement' must be declared in an enclosing class
x.#auditIncrement

// works
x.increment();

This feature has to do with class static blocks and ergonomic checks for private classes, as we’ll see in what follows.

https://github.com/tc39/proposal-class-fields

 

Class Static Block

As part of the new specification, we can now include static blocks in any class; they will only run once, and are a great way to decorate or perform initialization of some fields on the static side of a class.

We are not limited to using one block; we can have as many blocks as we want.

// will output 'one two three'
class A {
  static {
      console.log('one');
  }
  static {
      console.log('two');
  }
  static {
      console.log('three');
  }
}

They have a nice bonus, they get privileged access to private fields, and you can do some interesting patterns with them.

let getPrivateField;

class A {
  #privateField;
  constructor(x) {
    this.#privateField = x;
  }
  static {
    // it can access any private field
    getPrivateField = (a) => a.#privateField;
  }
}

const a = new A('foo');
// Works, foo is printed
console.log(getPrivateField(a));

If we try to access that private variable from the outer scope of the instance object, we will get Unable to read private member #privateField from an object whose class does not declare it.

https://github.com/tc39/proposal-class-static-block

 

Private Fields

The new private field is a great feature, however, it might become convenient to check if a field is private in some static methods.

Attempting to call it outside the class scope results in the same error we saw earlier.

class Foo {
  #brand;

  static isFoo(obj) {
    return #brand in obj;
  }
}

const x = new Foo();

// works, it returns true
Foo.isFoo(x);

// works, it returns false
Foo.isFoo({})

// Uncaught SyntaxError: Private field '#brand' must be declared in an enclosing class
#brand in x

https://github.com/tc39/proposal-private-fields-in-in