ES2022: What’s new for JavaScript?

OpenJavaScript 0
Reading Time: 6 minutes 🕑

Last updated: April 6, 2022.

The new version of JavaScript, ES2022, introduces many useful new features.

A large set of upgrades focus on classes, extending their existing functionality. There are also some easy wins, such as negative indexing of array with .at() and the introduction of top-level await within modules.

Note that these remain experimental features until fully implemented in browsers. Be sure to consult CanIUse to check browser compatibility before implementing these brand new features in your project.

Top-level await

Until now, it has only been possible to use the keyword await of a promise inside an asynchronous function, i.e. a function prepended by the keyword async.

ES2022 makes it possible to await a promise outside an asynchronous function within the content of JavaScript modules.

Top-level await effectively turns a child module into an asynchronous function. A parent module that is importing from it will then wait for its promises to resolve before importing from it.

For example, if you run a fetch request in a child module and use the keyword await to wait for its result before exporting, the parent module will not process the import until the fetch request is resolved.

// child.js
const data = fetch('https://httpbin.org/get')
  .then(res => res.json())
  .catch(err => err);
  
export default await data;

// parent.js
import data from './script.js';

console.log(data);

Using await in this way does not block the processing of other imports.

Note that modules require you to add the type="module" attribute to the linking HTML <script> tag and do not run locally. To test in Virtual Studio Code, you can use the Live Server extension to quickly create a test server.

.at()

A simple but very useful new feature is the introduction of the .at() array method.

Unlike square brackets [], the .at() method supports negative indexing.

So with .at(), it is possible to pass in a negative or possible index value:

const arr = ["JavaScript", "HTML", "ES2022", "CSS", "Chrome", "W3"];

arr.at(1); // "HTML"
arr.at(4); // "Chrome"
arr.at(-1); // "W3"
arr.at(-3); // "CSS"

Note that an index value of -1 signifies the element before element 0 in a circular array. So -1 returns the last element in the array.

Class upgrades

Public and private fields

ES2022 introduces public and truly private class fields: properties that can only be accessed by methods contained within a class.

Previously, properties were designated as private by naming convention only: prepended with an underscore.

As you can see in the following example, this doesn't prevent a private property from being accessed. And no error is thrown.

class User {
    constructor(username, password) {
      this.username = username;
      this._password = password;
    }
    getPassword() {
        return this._password;
    }
}

const testUser =  new User("test_user", "changeme");

console.log(testUser._password); // changeme

ES2022 solves this with public and private class fields. These are defined at the very beginning of a class definition, before the constructor.

A private field is designated by prepending #. The value of the private field cannot then be accessed outside an object instance of the class:

class User {
    username // public field 
    #password // private field
    constructor(username, password) {
      this.username = username;
      this.#password = password;
    }
    getPassword() {
        return this.#password;
    }
}

const testUser =  new User("test_user", "changeme");

console.log(testUser.#password); // Syntax Error

But it can still be accessed by methods within the class object:

console.log(testUser.getPassword()) // changeme

Private methods

You can also make methods private by appending #. This makes a method only accessible within an object instance of the class.

Here is a practical example, where the value of password is stored in a private field, and it can only be retrieved by running the getPassword method, which is only made private.

Only by running the public requestPassword method with the correct secret answer can the private getPassword method be run, returning the value of password:

class User {
    username // public field 
    #password // private field
    constructor(username, password) {
      this.username = username;
      this.#password = password;
    }
    #getPassword() { // private method
        return this.#password;
    }
    requestPassword(secretAnswer) {
        if (secretAnswer === "Fluffy") {
            return this.#getPassword()
        } else {
            return "Sorry, that's not the right answer."
        }
    }
}

const testUser =  new User("test_user", "changeme");

testUser.requestPassword("Fluffy") // changeme

Static and static private properties and methods

Static properties and methods are defined on a class, but not available on objects produced by the class.

Until now, these could be appended after the definition of a class:

class User {
    username
    #password
    isAdmin
    constructor(username, password, isAdmin) {
      this.username = username;
      this.#password = password;
      this.isAdmin = isAdmin;
    }
}

User.onlyAdmins = function(usersArray) {
    return usersArray.filter((user) => {
        return user.isAdmin === true;
    }) 
}

const users = [new User("Kevin", "myPassword", true), new User("Melanie", "Kenya63", false), new User("Brian", "#abc123", false)];

User.onlyAdmins(users); // Returns user object for Kevin only

ES2022 introduces the static keyword, which makes it possible to define static properties and methods within the definition of a class:

class User {
    username
    #password
    isAdmin
    constructor(username, password, isAdmin) {
      this.username = username;
      this.#password = password;
      this.isAdmin = isAdmin;
    }
    static onlyAdmins(users) {
        return users.filter((user) => {
            return user.isAdmin === true;
        })
    }
}

const users = [new User("Kevin", "myPassword", true), new User("Melanie", "Kenya63", false), new User("Brian", "#abc123", false)];

User.onlyAdmins(users); // Returns user object for Kevin only

Static properties and methods can also be made private by appending #:

class User {
    username
    #password
    isAdmin
    constructor(username, password, isAdmin) {
      this.username = username;
      this.#password = password;
      this.isAdmin = isAdmin;
    }
    static #onlyAdmins(users) {
        return users.filter((user) => {
            return user.isAdmin === true;
        })
    }
    static findAdmins(password, users) {
        if (password === "123") {
            return this.#onlyAdmins(users);
        } else {
            return "That's not the right password";
        }
    }
}

const users = [new User("Kevin", "myPassword", true), new User("Melanie", "Kenya63", false), new User("Brian", "#abc123", false)];

User.findAdmins("abc123", users); // Returns Kevin user object only

User.findAdmins("abc456", users); // "That's not the right password"

// User.#onlyAdmins(users); // SyntaxErrror

Private field check

Previously, if you wanted to check if a private field exists in an object produced by a class, you could add a static method to the class definition checking for this using try...catch syntax. Then, pass in an object instance of the class to check.

The problem: if the catch statement runs, it is not clear whether the private field doesn't exist or there is an error within the private field.

ES2022 provides a better check for if a private field exists. Using the in operator, false is not returned if there is an error within an existing private field. The syntax for the check is now also cleaner:

class User {
    #password;
    get #getPassword() {
        if (!this.#password) {
            throw Error("No password set for this user!")
        }
        return this.#password;
    }

    static newCheck(obj) { // ES2022 way
        return #getPassword in obj;
    }

    static oldCheck(obj){ // Old way
        try {
            obj.#getPassword;
            return true;
        } catch {
            return false; 
        }
    }
}

const newUser = new User();

console.log(User.newCheck(newUser)); // true
console.log(User.oldCheck(newUser)); // false

Static class blocks

Another ES2022 update to classes is the addition of static class blocks.

These blocks are executed at run-time and allow for more dynamic assignment of static class property values.

In the example below, a static class block is used to set the value of the static field resetPassword. If successful, it becomes the value of the variable dynamic. If not, it is set to "default".

const dynamic = "myPassword";

class User {
    static resetPassword;
    static {
        try {
            this.resetPassword = dynamic;   
        } catch {
            this.resetPassword = "default";
        }
    }
}

console.log(User.resetPassword); // "myPassword"

Object.hasOwn()

Until now, it has been possible to check is a property exists on an object by using the hasOwnProperty method.

const myObject = {
    prop1: "value",
}

myObject.hasOwnProperty('prop1');

Most of the time, it will run the check you intend. But it has some flaws.

For example, if you create an object with Object.create(null), it will have no prototype properties. Therefore hasOwnProperty will not exist!

Second, hasOwnProperty is not a protected property on the prototype of an object, and can thus be overwritten.

To avoid these problems, ES2022 introduces the hasOwn method on the globally available Object.

Object.hasOwn accepts two arguments. The first is the object and the second the property to check:

const myObject = Object.create(null);
myObject.prop1 = "value";

Object.hasOwn(myObject, 'prop1'); // true
// but
myObject.hasOwnProperty(myObject); // Uncaught TypeError: myObject.hasOwnProperty is not a function

The cause property for an Error object

ES2022 introduces the possibility of obtaining more information about an error, which is especially useful for nested errors.

For example, in the function below, there is a typo in console.log(), which will trigger the catch statement:

function formatArticles(articles) {
    return articles.map((article) => {
        try {
            cosole.log("Do something to each article");
        } catch (err) {
            throw new Error(`An error occured while formatting the articles`)
        }   
    })
}

formatArticles(["Article1", "Article2", "Article3"]);

But not much information about the error is printed to the console, other than the hard-coded error message and the fact the error occurs in the formatArticles function.

To get more precise information about the error, a second argument can be added to a new Error object. It should be an object containing a cause property. The value of the cause should be set to the catch error:

function formatArticles(articles) {
    return articles.map((article) => {
        try {
            cosole.log("Do something to each article");
        } catch (err) {
            throw new Error(`An error occured while formatting the articles`, {cause: err})
        }   
    })
}

formatArticles(["Article1", "Article2", "Article3"]);

RegExp match indices

Previously, regular expression matches would return the starting index of a match or set of matches.

By adding a d flag to a regular expression, the start and end indices of a match or set of matches is also returned:

const programmingTalk = "What about JavaScript...Linux...Java"

const regex = /(Java)/gd; // with g and now d

const matches = [...programmingTalk.matchAll(regex)];

matches[0].indices[0]; // [ 11, 15 ]
matches[1].indices[0]; // [ 32, 36 ]