The New ES13 Specification Is Finally Released.
JavaScript is not an open source language. It is a language that needs to be written in compliance with the ECMAScript standard specification. The TC39 committee is responsible for discussing and approving the release of new features. So who are they?
“ECMA International’s TC39 is a group of JavaScript developers, implementers, academics, etc. who work with the community to maintain and evolve the definition of JavaScript.” — TC39.es
Their release process consists of five stages, and they’ve been doing annual releases since 2015, which usually happen in the spring.
There are two ways to reference any ECMAScript version:
- By year: This new version will be ES2022.
- By its iteration count: This new version will be iteration 13, so it could be called ES13.
So what’s new in this version this time? What features can we be excited about?
01. Regular expression matching index
Currently, when using the JavaScript Regex API in JavaScript, only the starting index of the match is returned. However, for some special advanced scenarios, this is not enough.
As part of these specifications, a special flag d was added. By using it, the regular expression API will return a two-dimensional array as the key of the name index. It contains the starting and ending 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, with the named group name being 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.gr
oups['B'])
// prints [3, 6]
02. Top-level await
Prior to this proposal, Top-level await was not accepted, but there were workarounds to simulate this behavior, which had drawbacks.
The Top-level await feature lets us rely on modules to handle these Promises. This is an intuitive feature.
Note, however, that it may change the order of module execution. If a module depends on another module with a Top-level await call, the module’s execution will be suspended until the promise is completed.
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 and let’s not abuse it.
03..at()
JavaScript has long been requested to provide Python-like negative index accessors for arrays. Instead of doing array[array.length-1] do simply array[-1]. This is not possible because the [] symbol is also used for objects in JavaScript.
The accepted proposal took a more practical approach. Array objects 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
By the way, since we’re talking about arrays, did you know 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
04. Accessible Object.prototype.hasOwnProperty
The following is just a nice simplification, already having hasOwnProperty. However, it needs to be called within the lookup instance we want to perform. Therefore, it’s common 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" };
// ✅ using the new Object method
if (Object.hasOwn(x, "foo")) {
...
}
05.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 correctly can become challenging, and when catching and re-throwing them we lose the stack trace information.
There is no clear agreement on how to handle this, 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 these new specifications, we can construct a new error and retain a reference to the obtained error. We just pass the object {cause: err} to the Errorconstructor.
It all becomes simpler, standard and easier 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();
06. Class Fields
Prior to this version, there was no proper way to create a private field, there were some ways around it by using hoisting, but it was not a proper private field. But now it’s easy, 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();
Having private fields means we have strong encapsulation boundaries and class variables cannot be accessed from outside, which shows that the class keyword is no longer just sugar syntax.
We can also create private methods:
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();
This feature is related to class static blocks and ergonomic checks for private classes, which we will see next.
07. 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 certain fields on the static side of the class.
We are not limited to using one block, we can have as many 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 gain 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 did not declare it.
08.Private Fields
The new private fields are a great feature, however, it might become convenient to check if a field is private in some static methods.
Trying to call it outside the class scope will result in the same error we saw before.
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
final thoughts
This is an interesting version that provides many small but useful features, such as at, private fields and error cause. Of course, error cause will bring a lot of clarity to our daily error tracking tasks.
Some advanced features, like top-level await, need to be well understood before using them. They can cause unwanted side effects in the execution of your code.
I hope this article makes you as excited about the new ES2022 specification as I am, please remember to like and follow me.