The 5 most transformative JavaScript features from ES9

Last updated on May 04, 2024
The 5 most transformative JavaScript features from ES9

JavaScript has come a long way in the past 10 years with brand new feature upgrades in each one.

Let’s look at the 5 most significant features that arrived in ES9; and see the ones you missed.

1. Async generation and iteration

Async generators was a powerful one from ES9.

Just like normal generators but now it pops out the values after asynchronous work like a network request or something:

function* asyncGenerator() {
  yield new Promise((resolve) =>
    setTimeout(() => resolve('done this ✅'), 2000)
  yield new Promise((resolve) =>
    setTimeout(() => resolve('done that ✅'), 3000)

So when we call .next() we'll get a Promise:

const asyncGen = asyncGenerator();;;

It's such a powerful tool for streaming data in web app in a structured + readable manner — just look at this function that buffers and streams data for a video-sharing app like YouTube:

async function* streamVideo({ id }) {
  let endOfVideo = false;
  const downloadChunk = async (sizeInBytes) => {
    const response = await fetch(
    const { chunk, done } = await response.json();
    if (done) endOfVideo = true;
    return chunk;
  while (!endOfVideo) {
    const bufferSize = 500 * 1024 * 1024;
    yield await downloadChunk(bufferSize);

And now to consume this generator we'll use for await of -- async iteration:

for await (const chunk of streamVideo({ id: 2341 })) {
  // process video chunk

I wonder if the actual YouTube JavaScript code uses generators like this?

2. Rest / spread operator

No doubt you've stumbled upon the modern spread syntax somewhere.

A genius way to rapidly and immutable clone arrays:

const colors = ['🔴', '🔵', '🟡'];

console.log([...colors, '🟢']);
// [ '🔴', '🔵', '🟡', '🟢' ]

We never had it before ES9, and now it's all over the place.

Redux is big one:

export default function userState(state = initialUserState, action){
    switch (action.type){
      case ADD_ITEM :
        return { 
             arr:[...state.arr, action.newItem]

      default:return state

And it works for objects too:

const info = {
  name: 'Coding Beauty',
  site: '',

console.log({, theme: '🔵' });
// { name: 'Coding Beauty',
//   site: '',
//   theme: '🔵' }

Overrides props:

const langs = {
  j: 'java',
  c: 'c++',

console.log({ ...langs, j: 'javascript ' });
// { j: 'javascript ', c: 'c++' }

This makes it especially great for building upon default values, especially when making a public utility.

Or customizing a default theme like I did with Material UI:

With the spread syntax you can even scoop out an object's copy without properties you don't want.

const colors = {
  yellow: '🟡',
  blue: '🔵',
  red: '🔴',

const { yellow, ...withoutYellow } = colors;

// { blue: '🔵', red: '🔴' }

That's how you remove properties from an object immutably.

3. String.raw

When I use String.raw I'm saying: Just give me what I give you. Don't process anything.

Leave those escape characters alone:

// For some weird reason you can use it without brackets
// like this 👇

const message = String.raw`\n is for newline and \t is for tab`;
console.log(message); // \n is for newline and \t is for tab

No more escaping backslashes:

const filePath = 'C:\\Code\\JavaScript\\tests\\index.js';

console.log(`The file path is ${filePath}`);

// The file path is C:\Code\JavaScript\tests\index.js

We can write:

const filePath = String.raw`C:\Code\JavaScript\tests\index.js`;

console.log(`The file path is ${filePath}`);

// The file path is C:\Code\JavaScript\tests\index.js

Perfect for writing regexes with a stupid amount of these backslashes:

Something like this but much worse:

From this✅:

const patternString = 'The (\\w+) is (\\d+)';
const pattern = new RegExp(patternString);

const message = 'The number is 100';
// ['The number is 100', 'number', '100']

To this✅:

const patternString = String.raw`The (\w+) is (\d+)`;
const pattern = new RegExp(patternString);

const message = 'The number is 100';
// ['The number is 100', 'number', '100']

So "raw" as in unprocessed.

That's why we have String.raw() but no String.cooked().

4. Sophisticated regex features

And speaking of regexes ES9 didn't disappoint.

It came fully loaded with state-of-the-art regex features for advanced string searching and replacing.

Look-behind assertions

This was a new feature to make sure that only a certain pattern comes before what you're searching for:

  • Positive look-behind: Whitelist ?<=pattern
  • Negative look-behind: Blacklist ?
const str = "It's just $5, and I have €20 and £50";

// Only match number sequence if $ comes first
const regexPos = /(?<=\$)\d+/g;

console.log(str.match(regexPos)); // ['5']

const regexNeg = /(?<!\$)(\d+)/g; // ['20', '50' ]


Named capture groups

Capture groups has always been one of the most invaluable regex features for transforming strings in complex ways.

const str = 'The cat sat on a map';

// $1 -> [a-z]
// $2 -> a
// $3 -> t

// () indicates group
str.replace(/([a-z])(a)(t)/g, '$1*$3');
// -> The c*t s*t on a map

So normally the groups go by their relative position in the regex: 1, 2, 3...

But this made understanding and changing those stupidly long regexes much harder.

So ES9 solved this with ?<name> to name capture groups:

const str = 'The cat sat on a map';

// left & right
console.log(str.replace(/(?<left>[a-z])(a)(?<right>t)/g, '$<left>*$<right>'));
// -> The c*t s*t on a map

You know how when things break in VS Code, you can quickly Alt + Click to go to the exact point where it happened? 👇

VS Code uses capture groups to make the filenames clickable and make this rapid navigation possible.

I'd say it's something like this:

// The stupidly long regex
const regex =
  /(?<path>[a-z]:(?:(?:\/|(?:\\?))[\w \.-]+)+):(?<line>\d+):(?<char>\d+)/gi;

// ✅ String.raw!
const filePoint = String.raw`C:\coding-beauty\coding-beauty-javascript\index.js:3:5`;

const extractor =
  /(?<path>[a-z]:(?:(?:\/|(?:\\?))[\w \.-]+)+):(?<line>\d+):(?<char>\d+)/i;

const [path, lineStr, charStr] = filePoint
  .slice(1, 4);

const line = Number(lineStr);
const char = Number(charStr);

console.log({ path, line, char });

// Replace all filePoint with <button> tag
// <button onclick="navigateWithButtonFilepointInnerText">{filePoint}</button>

5. Promise.finally

Finally we have Promise.finally 😉.

You know how finally always run some code whether errors are there or not?

function startBodyBuilding() {
  if (Math.random() > 0.5) {
    throw new Error("I'm tired😫");
  console.log('Off to the gym 👟💪');

try {
} catch {
  console.log('Stopped excuse🛑');
} finally {
  console.log("I'm going!🏃");

So Promise.finally is just like that but for async tasks:

async function startBodyBuilding() {
  await think();
  if (Math.random() > 0.5) {
    throw new Error("I'm tired😫");
  console.log('Off to the gym 👟💪');

  .then(() => {
    console.log('Started ✅');
  .catch(() => {
    console.log('No excuses');
  .finally(() => {
    console.log("I'm going!🏃");

The biggest pro of Promise.finally() is when you're chaining lots of Promises:

It also works well with Promise chains:

getFruitApiUrl().then((url) => {
  return fetch(url)
    .then((res) => res.json())
    .then((data) => {
    .catch((err) => {
    .finally(() => {

Brought forth by ES9.

Final thoughts

ES9 marked a significant leap forward for JavaScript with several features that have become essential for modern development.

Empowering you to write cleaner code with greater conciseness, expressiveness, and clarity.

See also