ES2022 Preview: 10 Exciting JavaScript Language Features From 2021
Tags: javascript, programming, node, webdev
Published: 2021-12-30 21:07:56

JavaScript evolves quickly. In 2021, several [proposals](https://www.proposals.es/) have moved to Stage 4 of the [TC39 process](https://tc39.es/process-document/) 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](https://github.com/tc39/proposal-class-fields), [2](https://github.com/tc39/proposal-private-methods)\] 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:
```javascript
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](https://caniuse.com/mdn-javascript_classes_private_class_fields) (Dec 2021 usage: ~90%) and Node.js 12+ support private instance fields. The [support for private methods and accessors is more limited in browsers](https://caniuse.com/mdn-javascript_classes_private_class_methods) (Dec 2021 usage: ~80%). Node.js has supported the feature since version 14.6. You can [transpile your code with Babel](https://blog.saeloun.com/2021/06/24/babel-enables-class-field-and-private-methods.html) 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](https://github.com/tc39/proposal-private-fields-in-in)**:

```javascript
class Example {
  #field

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

The [browser support for using the `in` operator on private fields](https://caniuse.com/mdn-javascript_classes_private_class_fields_in) 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](https://babeljs.io/docs/en/babel-plugin-proposal-private-property-in-object).

### Public Static Class Fields

**[Static class fields](https://github.com/tc39/proposal-static-class-features#static-public-fields) are a convenient notation for adding properties to the class object.**

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

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

[Most browsers](https://caniuse.com/mdn-javascript_classes_static_class_fields) (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](https://github.com/tc39/proposal-static-class-features) adds hard visibility limitations for class-level fields and methods using the `#` prefix.**

```javascript
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](https://github.com/tc39/proposal-class-static-block) 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:

```javascript
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](https://caniuse.com/mdn-javascript_classes_static_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](https://babeljs.io/docs/en/babel-plugin-proposal-class-static-block).

### 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](https://github.com/tc39/proposal-top-level-await)** makes it easy to use `await` in CLI scripts (e.g., with `.mjs` sources and [zx](https://github.com/google/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:
```javascript
// load-attribute.mjs 
// with top-level await
const data = await (await fetch("https://some.url")).text();
export const attribute = JSON.parse(data).someAttribute;
```

```javascript
// 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](https://caniuse.com/mdn-javascript_operators_await_top_level) (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`](https://webpack.js.org/configuration/experiments/).

### 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](https://github.com/tc39/proposal-error-cause) 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.

```javascript
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](https://caniuse.com/mdn-javascript_builtins_error_cause) is limited (Dec 2021 usage: ~70%). Node.js supports the feature since version 16.9. You can use the [error cause polyfill](https://github.com/es-shims/error-cause) 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](https://github.com/tc39/proposal-relative-indexing-method) 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.**

```javascript
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](https://caniuse.com/mdn-javascript_builtins_array_at) 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](https://github.com/zloirock/core-js#relative-indexing-method) in the meantime.

### Object: .hasOwn()
**The [Object.hasOwn feature](https://github.com/tc39/proposal-accessible-object-hasownproperty) 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`:

```javascript
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](https://caniuse.com/mdn-javascript_builtins_object_hasown) (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`](https://github.com/zloirock/core-js/#accessible-objectprototypehasownproperty).

### 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)](https://github.com/tc39/proposal-regexp-match-indices), 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:
```javascript
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:
```json
[
  '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](https://caniuse.com/mdn-javascript_builtins_regexp_hasindices) (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](https://github.com/rbuckton/regexp-match-indices) 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!