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.

Coding Beauty Assistant logo

Try Coding Beauty AI Assistant for VS Code

Meet the new intelligent assistant: tailored to optimize your work efficiency with lightning-fast code completions, intuitive AI chat + web search, reliable human expert help, and more.

See also