ES2022: What’s new for JavaScript?
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 ]