ES2022 Preview: 10 Exciting JavaScript Language Features From 2021

ES2022 Preview: 10 Exciting JavaScript Language Features From 2021

By Lars Grammel
By Lars Grammel
Published on December 31, 2021

JavaScript evolves quickly. In 2021, several proposals have moved to Stage 4 of the TC39 process and will be included in ES2022. They add the following features to JavaScript:

Classes and Instances

  • Private instance fields, methods, and accessors
  • Existence checks for private fields
  • Public static class fields
  • Private static class fields and methods
  • Static class initialization blocks

Module Loading

  • Top-Level await

Built-in Objects

  • Error: .cause
  • Array, String, and TypedArray: .at()
  • Object: .hasOwn()
  • RegExp: match .indices ('d' flag)

This blog post describes each feature, shows an example of how it can be used, and looks at current browser and Node.js support (as of December 2021). Let's get started:

Private Instance Fields, Methods, and Accessors

Encapsulation is one of the core principles of object-oriented programming. It is usually implemented using visibility modifiers such as private or public.

The private instance fields, methods, and accessors features [1, 2] add hard visibility limitations to JavaScript. The # prefix marks a field, method, or accessor in a class as private, meaning that you cannot access it from outside the instances themselves.

Here is an example of a private field and method; accessors work similarly:

class Example {
  #value;

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

  #calc() {
    return this.#value * 10;
  }

  print() {
    console.log(this.#calc());
  }
}

const object = new Example(5);
console.log(object.#value);    // SyntaxError
console.log(object.#calc());   // SyntaxError
object.print();                // 50

Most browsers (Dec 2021 usage: ~90%) and Node.js 12+ support private instance fields. The support for private methods and accessors is more limited in browsers (Dec 2021 usage: ~80%). Node.js has supported the feature since version 14.6. You can transpile your code with Babel to use private class fields and methods on environments that don't directly support them.

Existence Checks For Private Fields

Since trying to access a non-existing private field on an object throws an exception, it needs to be possible to check if an object has a given private field. The in operator can be used to check if a private field is available on an object:

class Example {
  #field

  static isExampleInstance(object) {
    return #field in object;
  }
}

The browser support for using the in operator on private fields is limited (Dec 2021 usage: ~70%). Node.js supports the feature since version 16.4. You can transpile usages of the in operator for private fields with Babel.

Public Static Class Fields

Static class fields are a convenient notation for adding properties to the class object.

// without static class fields:
class Customer {
  // ...
}
Customer.idCounter = 1;

// with static class fields:
class Customer {
  static idCounter = 1;
  // ...
}

Most browsers (Dec 2021 usage: ~90%) and Node.js 12+ support public class fields.

Private Static Class Fields and Methods

Similar to private instance fields and methods, encapsulation and visibility limitations are helpful on the class level. The private static methods and fields feature adds hard visibility limitations for class-level fields and methods using the # prefix.

class Customer {
  static #idCounter = 1; // static private

  static #getNextId() { // static private
    return Customer.#idCounter++;
  }

  #id; // instance private

  constructor() {
    this.#id = Customer.#getNextId();
  }

  toString() {
    return `c${this.#id}`;
  }
}

const customers = [new Customer(), new Customer()];
console.log(customers.join(' ')); // c1 c2

The browser and Node.js support are similar to the private instance fields and methods above.

Static Class Initialization Blocks

Sometimes it is necessary or convenient to do more complex initialization work for static class fields. For the private static fields feature from above, this initialization must even happen within the class because the private fields are not accessible otherwise.

The static initializer blocks feature provides a mechanism to execute code during the class definition evaluation. The code in a block statement with the static keyword is executed when the class is initialized:

class Example {
  static propertyA;
  static #propertyB; // private

  static { // static initializer block
    try {
      const json = JSON.parse(fs.readFileSync('example.json', 'utf8'));
      this.propertyA = json.someProperty;
      this.#propertyB = json.anotherProperty;
    } catch (error) {
      this.propertyA = 'default1';
      this.#propertyB = 'default2';
    }
  }

  static print() {
    console.log(Example.propertyA);
    console.log(Example.#propertyB);
  }
}

Example.print();

The browser support for static class initialization blocks is limited (Dec 2021: ~70%). Node.js supports the feature since version 16.4. You can transpile code with static initializer blocks with Babel.

Top-Level Await

Async functions and the await keyword were introduced in ES2017 to simplify working with promises. However, await could only be used inside async functions.

The top-level await feature for ES modules makes it easy to use await in CLI scripts (e.g., with .mjs sources and zx), and for dynamic imports and data loading. It extends the await functionality into the module loader, which means that dependent modules will wait for async modules (with top-level await) to be loaded.

Here is an example:

// load-attribute.mjs 
// with top-level await
const data = await (await fetch("https://some.url")).text();
export const attribute = JSON.parse(data).someAttribute;
// main.mjs 
// loaded after load-attribute.mjs is fully loaded
// and its exports are available
import { attribute } from "./load-attribute.mjs";
console.log(attribute);

Top-level await is supported on modern browsers (Dec 2021 usage: ~80%) and Node.js 14.8+. It is only available for ES modules, and it is doubtful that CommonJS modules will ever get top-level await support. Code with top-level await can be transpiled during the bundling phase to support older browsers, such as Webpack 5 experiments.topLevelAwait = true.

Error: .cause

Errors are often wrapped to provide meaningful messages and record the error context. However, this means that the original error can get lost. Attaching the original error to the wrapping error is desirable for logging and debugging purposes.

The error cause feature provides a standardized way to attach the original error to a wrapping error. It adds the cause option to the Error constructor and a cause field for retrieving the original error.

const load = async (userId) => {
  try {
    return await fetch(`https://service/api/user/${userId}`);
  } catch (error) {
    throw new Error(
      `Loading data for user with id ${userId} failed`, 
      { cause: error }
    );
  }
}

try {
  const userData = await load(3);
  // ...
} catch (error) {
  console.log(error); // Error: Loading data for user with id 3 failed
  console.log(error.cause); // TypeError: Failed to fetch
}

The current browser support for the error clause feature is limited (Dec 2021 usage: ~70%). Node.js supports the feature since version 16.9. You can use the error cause polyfill to start using the feature today, even in JS environments where it is not supported.

Array, String, and TypedArray: .at()

Getting elements from the end of an array or string usually involves subtracting from array's length, for example, let lastElement = anArray[anArray.length - 1]. This requires that the array is stored in a temporary variable and prevents seamless chaining.

The .at() feature provides a way to get an element from the beginning (positive index) or the end (negative index) of a string or an array without a temporary variable.

const getExampleValue = () => 'abcdefghi';

console.log(getExampleValue().at(2));    // c
console.log(getExampleValue()[2]);       // c

const temp = getExampleValue();
console.log(temp[temp.length - 2]);      // h
console.log(getExampleValue().at(-2));   // h - no temp var needed

The browser support for the .at feature is currently limited (Dec 2021 usage: ~70%), and it is only available in Node.js 16.6+. You can use the .at() polyfill from Core JS in the meantime.

Object: .hasOwn()

The Object.hasOwn feature is a more concise and robust way of checking if a property is directly set on an object. It is a preferred alternative to using hasOwnProperty:

const example = {
  property: '123'
};

console.log(Object.prototype.hasOwnProperty.call(example, 'property'));
console.log(Object.hasOwn(example, 'property')); // preferred

The browser support is currently limited (Dec 2021 usage: ~70%), and you need Node 16.9+ to use hasOwn directly. In the meantime there is a Core JS polyfill for hasOwn.

RegExp: Match Indices ('d' Flag)

By default, regular expression matches record the start index of the matched text, but not its end index and not the start and end indices of its capture groups. For use cases such as text editor syntax or search result highlighting, having capture group match indices as part of a regular expression match can be helpful.

With the regexp match indices feature ('d' flag), the match and capture group indices are available in the indices array property of the regular expression result. The matched text position and the match indices position are the same, e.g., the full matched text is the first value in the match array and the indices array. The indices of the named captured groups are recorded in indices.groups.

Here is an example:

const text = "Let's match one:1.";
const regexp = /match\s(?<word>\w+):(?<digit>\d)/gd;

for (const match of text.matchAll(regexp)) {
    console.log(match);
}

The above example code has the following output:

[
  'match one:1',
  'one',
  '1',
  index: 6,
  input: "Let's match one:1.",
  groups: { word: 'one', digit: '1' },
  indices: {
    0: [6,17],
    1: [12,15],
    2: [16,17],
    groups: { 
      digit: [16, 17],
      word: [12, 15]
    }
  }
]

The browser support for the RegExp match indices feature is currently limited (Dec 2021 usage: ~80%). In Node.js, you can activate the feature with the --harmony-regexp-match-indices flag, but it is disabled by default. You can use the RegExp match indices polyfill in the meantime.

Conclusion

The new JavaScript from 2021 features help make development more convenient and robust, and most of them already work on the latest browsers and Node.js environments.

However, many users are still on browsers and environments without full ES2022 support. For production use, it is essential to check the target environments and use polyfilling and transpiling as needed or to wait a bit longer before using the new features.

Happy coding in 2022!