Top 10 new JavaScript features from 2021 to 2023

Last updated on January 03, 2024
Top 10 new JavaScript features from 2021 to 2023

These 10 amazing features were all about writing shorter, safer, and more expressive code.

1. Private methods and fields

We need our privacy. There's no two ways about it.

And so do OOP classes; but in JavaScript it was once impossible to strictly declare private members.

It was once impossible to declare private members in a JavaScript class.

A member was traditionally prefixed with an underscore (_) to indicate that it was meant to be private, but it could still be accessed and modified from outside the class.

class Person {
  _firstName = 'Coding';
  _lastName = 'Beauty';

  get name() {
    return `${this._firstName} ${this._lastName}`;
  }
}

const person = new Person();
console.log(person.name); // Coding Beauty

// Members intended to be private can still be accessed
// from outside the class
console.log(person._firstName); // Coding
console.log(person._lastName); // Beauty

// They can also be modified
person._firstName = 'Debugging';
person._lastName = 'Nightmares';

console.log(person.name); // Debugging Nightmares

With ES2022, we can now add private fields and members to a class, by prefixing it with a hashtag (#). Trying to access them from outside the class will cause an error:

class Person {
  #firstName = 'Coding';
  #lastName = 'Beauty';

  get name() {
    return `${this.#firstName} ${this.#lastName}`;
  }
}

const person = new Person();
console.log(person.name);

// SyntaxError: Private field '#firstName' must be
// declared in an enclosing class
console.log(person.#firstName);
console.log(person.#lastName);

Note that the error thrown here is a syntax error, which happens at compile time; the code doesn't run at all; the compiler doesn't expect you to even try to access private fields from outside a class, so it assumes you're trying to declare one.

"Ergonomic brand" checks for private fields

With private fields come a new keyword to safely check if a class object contains a particular one -- the in keyword:

class Car {
  #color;
  hasColor() {
    return #color in this;
  }
}
const car = new Car();
console.log(car.hasColor()); // true;

It correctly distinguishes private fields with the same names from different classes:

class Car {
  #color;
  hasColor() {
    return #color in this;
  }
}
class House {
  #color;
  hasColor() {
    return #color in this;
  }
}

const car = new Car();
const house = new House();
console.log(car.hasColor()); // true;
console.log(car.hasColor.call(house)); // false
console.log(house.hasColor()); // true
console.log(house.hasColor.call(car)); // false

And don't ask me about the name; I also have no idea why they're called that (do you?).

Ergonomics as far as I'm concerned is all about keeping good sitting posture while using your computer (?) 🤔.

Although I guess you could twist this definition to allow for this new feature -- or any new feature for that matter. They're all about comfort, right? Less pain writing code.

But I guess English isn’t a closed language and you can always add new words and additional definitions (just ask Shakespeare).

2. Immutable sort(), splice(), and reverse()

ES2023 came fully packed with immutable versions of these 3 heavily used array methods.

Okay maybe splice() isn't used as much as the others, but they all mutate the array in place.

const original = [5, 1, 3, 4, 2];

const reversed = original.reverse();
console.log(reversed); // [2, 4, 3, 1, 5] (same array)
console.log(original); // [2, 4, 3, 1, 5] (mutated)

const sorted = original.sort();
console.log(sorted); // [1, 2, 3, 4, 5] (same array)
console.log(original); // [1, 2, 3, 4, 5] (mutated)

const deleted = original.splice(1, 2, 7, 10);
console.log(deleted); // [2, 3] (deleted elements)
console.log(original); // [1, 7, 10, 4, 5] (mutated)

Immutability gives us predictable and safer code; debugging is much easier as we're certain variables never change their value.

Arguments are exactly the same, with splice() and toSpliced() having to differ in their return value.

const original = [5, 1, 3, 4, 2];

const reversed = original.toReversed();
console.log(reversed); // [2, 4, 3, 1, 5] (copy)
console.log(original); // [5, 1, 3, 4, 2] (unchanged)

const sorted = original.toSorted();
console.log(sorted); // [1, 2, 3, 4, 5] (copy)
console.log(original); // [5, 1, 3, 4, 2] (unchanged)

const spliced = original.toSpliced(1, 2, 7, 10);
console.log(spliced); // [1, 7, 10, 4, 5] (copy)
console.log(original); // [5, 1, 3, 4, 2] (unchanged)

3. Top-level await

Did you know: F# was the first language to introduce async/await? As far back as 2007! But it took JavaScript 10 good years to catch up.

await pauses execution in the async context until a Promise resolves.

Previously we could only use this operator in an async function, but it could never work in the global scope.

function setTimeoutAsync(timeout) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, timeout);
  });
}

// SyntaxError: await is only valid in async functions
await setTimeoutAsync(3000);

With ES2022, now we can:

function setTimeoutAsync(timeout) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, timeout);
  });
}

// ✅ Waits for timeout - no error thrown
await setTimeoutAsync(3000);

4. Promise.any()

If you know Promise.all(), then you can easily guess what this does: wait for one Promise to resolve and return the result.

async function getHelpQuickly() {
  const response = await Promise.any([
    cautiousHelper(),
    kindHelper(),
    wickedHelper(),
  ]);
  console.log(response); // Of course!
}

async function cautiousHelper() {
  await new Promise((resolve) => {
    setTimeout(() => {
      resolve('Uum, oohkaay?');
    }, 2000);
  });
}

async function kindHelper() {
  return 'Of course!';
}

function wickedHelper() {
  return Promise.reject('Never, ha ha ha!!!');
}

Point to note: Promise.any() still waits for *all* the promises in the current async context to resolve, even though it only returns the result of the first one.

await getHelpQuickly(); // outputs "Of course!" immediately
// Still waits for 2 seconds

5. Array find from last

Array find() searches for an array element that passes a specified test condition, and findIndex() gets the index of such an element.

While find() and findIndex() both start searching from the first element of the array, there are instances where it would be preferable to start the search from the last element instead.

There are scenarios where we know that finding from the last element might achieve better performance. For example, here we're trying to get the item in the array with the value prop equal to y. With find() and findIndex():

const letters = [
  { value: 'v' },
  { value: 'w' },
  { value: 'x' },
  { value: 'y' },
  { value: 'z' },
];

const found = letters.find((item) => item.value === 'y');
const foundIndex = letters.findIndex((item) => item.value === 'y');

console.log(found); // { value: 'y' }
console.log(foundIndex); // 3

This works, but as the target object is closer to the tail of the array, we might be able to make this program run faster if we use the new ES2022 findLast() and findLastIndex() methods to search the array from the end.

const letters = [
  { value: 'v' },
  { value: 'w' },
  { value: 'x' },
  { value: 'y' },
  { value: 'z' },
];

const found = letters.findLast((item) => item.value === 'y');
const foundIndex = letters.findLastIndex((item) => item.value === 'y');

console.log(found); // { value: 'y' }
console.log(foundIndex); // 3

Another use case might require that we specifically search the array from the end to get the correct item.

If we're finding the last even number in a list of numbers, find() and findIndex() produces a totally wrong result:

const nums = [7, 14, 3, 8, 10, 9];

// gives 14, instead of 10
const lastEven = nums.find((value) => value % 2 === 0);

// gives 1, instead of 4
const lastEvenIndex = nums.findIndex((value) => value % 2 === 0);

console.log(lastEven); // 14
console.log(lastEvenIndex); // 1

We could call the reverse() method on the array to reverse the order of the elements before calling find() and findIndex(). But this approach would cause unnecessary mutation of the array; reverse() reverses the elements of an array in place.

The only way to avoid this mutation would be to make a new copy of the entire array, which could cause performance problems for large arrays.

Also findIndex() still wouldn't on the reversed array, as reversing the elements would also mean changing the indexes they had in the original array. To get the original index, we would need to perform an additional calculation, which means writing more code.

const nums = [7, 14, 3, 8, 10, 9];

// Copying the entire array with the spread syntax before
// calling reverse()
const reversed = [...nums].reverse();

// correctly gives 10
const lastEven = reversed.find((value) => value % 2 === 0);

// gives 1, instead of 4
const reversedIndex = reversed.findIndex((value) => value % 2 === 0);

// Need to re-calculate to get original index
const lastEvenIndex = reversed.length - 1 - reversedIndex;

console.log(lastEven); // 10
console.log(reversedIndex); // 1
console.log(lastEvenIndex); // 4

It's cases like where the findLast() and findLastIndex() methods come in handy.

const nums = [7, 14, 3, 8, 10, 9];

const lastEven = nums.findLast((num) => num % 2 === 0);
const lastEvenIndex = nums.findLastIndex((num) => num % 2 === 0);

console.log(lastEven); // 10
console.log(lastEvenIndex); // 4

This code is shorter and more readable. Most importantly, it produces the correct result.

6. String replaceAll()

We already had replace() for quickly replace a substring within a string.

const str =
  'JavaScript is so terrible, it is unbelievably terrible!!';

const result = str.replace('terrible', 'wonderful');

console.log(result);
// JavaScript is so wonderful, it is unbelievably terrible!!
// Huh?

But it only does so for the first occurrence of the substring unless you use a regex; now we have replaceAll() to replace every single instance of that substring.

const str =
  'JavaScript is so terrible, it is unbelievably terrible.';

const result = str.replaceAll('terrible', 'wonderful');

console.log(result);

// JavaScript is wonderful, it is unbelievably wonderful.
// Now you're making sense!

7. Array with() and at()

at() came first and with() came a year after that in 2023.

They are the functional and immutable versions of single-element array modification and access.

const colors = ['pink', 'purple', 'red', 'yellow'];

console.log(colors.at(1)); // purple

console.log(colors.with(1, 'blue'));
// ['pink', 'blue', 'red', 'yellow']

// Original not modified
console.log(colors);
// ['pink', 'purple', 'red', 'yellow']

The cool thing about these new methods is how they let you get and change element values with negative indexing.

// index -N is same as index arr.length - N

const fruits = ['banana', 'apple', 'orange', 'butter???'];

console.log(fruits.at(-3)); // apple
console.log(fruits.at(-1)); // butter???

console.log(fruits.with(-1, 'watermelon'));
// ['banana', 'apple', 'orange', 'watermelon'] ✅

8. static static static

Static class fields, static private methods (2022).

Static methods access other private/public static members in the class using the this keyword; instance methods with this.constructor:

class Person {
  static #count = 0;
  static getCount() {
    return this.#count;
  }
  // Instance method
  constructor() {
    this.constructor.#incrementCount();
  }
  static #incrementCount() {
    this.#count++;
  }
}
const person1 = new Person();
const person2 = new Person();
console.log(Person.getCount()); // 2

Static blocks.

Executed only once when the *class* is created. It's like static constructors in other OOP languages like C# and Java.

class Vehicle {
  static defaultColor = 'blue';
}
class Car extends Vehicle {
  static colors = [];
  static {
    this.colors.push(super.defaultColor, 'red');
  }
  static {
    this.colors.push('green');
  }
}
console.log(Car.colors); // [ 'blue', 'red', 'green' ]

When they're multiple static blocks, they're executed in the order they're declared, along with any static fields in between. The super property in a static block to access properties of the superclass.

9. Logical assignment operators

They let a variable perform a logical operation with another variable and re-assign the result to itself.

We use them like this:

left ??= right;
left ||= right;
left &&= right;

They're as good as:

// More like exactly the same as
left = (left ?? right);
left = (left || right);
left = (left && right);

??=. Quickly assign a value to a variable *if* it is null or undefined ("nullish").

user.preferredName ??= generateDumbUserName();

||=. Like ??=, but assigns the value for any falsy value (0, undefined, null, '', NaN, or false).

user.profilePicture ||= "/angry-stranger.png";

And then &&=. Something like the reverse; only assigns when the value is truthy (not falsy).

user.favoriteLanguage = await setFavoriteLanguage(input.value);

user.favoriteLanguage &&= 'Assembly'; // You're lying! It's Assembly!

10. Numerical separators

Tiny new addition to make big number literals more readable and human-friendly.

const isItPi = 3.1_415_926_535;

const isItAvagadro = 602_214_076_000_000_000_000_000;

const isItPlanck = 6.626_070_15e-34;

const isItG = 6.674_30e-11;

// Works for other number bases too...

The compiler completely ignores those pesky underscores -- they're all for you, the human!

Final thoughts

These are the juicy new JavaScript features that arrived in the last 3 years. Use them to boost your productivity as a developer and write cleaner code with greater conciseness, expressiveness and clarity.

Supercharge your online security with Surfshark VPN - 82% off

Surfshark logo

Minimize the effort, sacrifice, and time delay of securing your online activities. Sign up for Surfshark VPN now and enjoy 82% off + 4 months free! Don't let your privacy be compromised.

See also