cf.completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

Modern JavaScript: ES2015 to today

Learn what each yearly JavaScript edition added from ES2015 to ES2026, feature by feature, with runnable examples of the ones you will use most.

By the end, you can
  • 01
    Place a feature in timeSay roughly which edition added a feature such as ?., toSorted or Object.groupBy.
  • 02
    Use the everyday onesWrite code with destructuring, async functions, ?. and ??, private fields and the newest Set methods.
  • 03
    Modernize old codeRewrite ES5-style code with modern features, and check that it prints the same result.

Modern JavaScript: a new edition every year

"Modern JavaScript" usually means the language from ES2015 on. ES2015 was one big edition. Since then, a smaller edition has come out every June, each with a handful of finished proposals. This lesson walks through them, year by year, with the features you will use most.

Features from three editions in four linesPop out in the code editor (opens in a new tab)JavaScript
const order = { item: "tea", prices: [5, 3, 4] };
const { item, prices } = order;
console.log(`${item}: ${prices.toSorted((a, b) => a - b).join(", ")}`);
console.log(order.customer?.name ?? "guest");

Line 1 makes an order. Line 2 uses destructuring, from ES2015, to copy item and prices out of the order into two variables.

Line 3 uses a template literal, also ES2015, and toSorted, from ES2023. It prints tea: 3, 4, 5. Line 4 uses ?. and ??, from ES2020. The order has no customer, so it prints guest.

A big card for ES2015 on the left, labeled the big one. On the right, eleven small cards, one for each year from 2016 to 2026, labeled a smaller edition every June.ES2015the big one20162017201820192020202120222023202420252026a smaller edition every June
ES2015 added more than any edition before or since. Since then, a smaller edition has come out every June. The example uses features from 2015, 2020 and 2023.

Read each row of the table as: "this edition added these main features." The list comes from TC39's own list of finished proposals.

What each edition added
The editionIts main features
ES2015let and const, arrow functions, classes, modules, promises, template literals, destructuring, Map and Set, for…of, generators.
ES2016The ** operator and Array.prototype.includes.
ES2017Async functions, Object.values and Object.entries, padStart and padEnd.
ES2018Object rest and spread, async iteration, Promise.prototype.finally, named groups in regular expressions.
ES2019flat and flatMap, Object.fromEntries, trimStart and trimEnd, catch without a name.
ES2020?., ??, BigInt, globalThis, Promise.allSettled and import().
ES2021replaceAll, Promise.any, ??= and the other logical assignments, 1_000 number separators.
ES2022Class fields and private # names, top-level await, at, Object.hasOwn, error causes.
ES2023toSorted, toReversed, with and findLast for arrays.
ES2024Object.groupBy and Map.groupBy, Promise.withResolvers, the regex v flag.
ES2025Iterator helpers, new Set methods, Promise.try, RegExp.escape, JSON modules.
ES2026Map.prototype.getOrInsert, Math.sumPrecise, Error.isError, Array.fromAsync, base64 for Uint8Array.
Real-life analogyA phone that updates every year

Picture a phone that went years without an update, then got one huge update that changed the home screen, the apps and the settings. After that, it gets a small update every year: a new button here, a faster menu there. Your old apps still open after each update.

In real life: One huge update that changed almost everything
In JavaScript: ES2015
In real life: A small update every year after that
In JavaScript: ES2016, ES2017 and onward
In real life: Old apps still open after every update
In JavaScript: Old code still runs in every new edition
The main idea

ES2015 laid the foundation: let, const, classes, modules, promises and arrow functions. Every edition since adds a few smaller tools, such as async functions, ?., private fields and new array and Set methods, and all of them run next to older code.

ES2015: the big foundation

ES2015, also called ES6, added more than any edition before it. Most modern code uses its features on every line.

let, const, for…of, destructuring and arrow functionsPop out in the code editor (opens in a new tab)JavaScript
const prices = [3, 4, 5];
let total = 0;
for (const price of prices) {
  total += price;
}
const [first, ...rest] = prices;
const label = (name, cost = 1) => `${name} costs ${cost}`;
console.log(total, first, rest.length);
console.log(label("tea", total), label("cake"));

Line 1 uses const, a variable that can't be given a new value. Line 2 uses let, which can. Lines 3 to 5 loop over the prices with for…of, which visits each value.

Line 6 unpacks the array: first gets 3, and ...rest collects the other items. Line 7 makes an arrow function with a default value: if no cost is passed, cost is 1, and the template literal puts both values into one string.

Line 8 prints 12 3 2. Line 9 prints tea costs 12 cake costs 1: the second call has no cost, so it uses the default.

A box labeled prices holds [3, 4, 5]. One arrow goes to a box labeled first, holding 3. Another goes to a box labeled rest, holding [4, 5].[3, 4, 5]prices3firstthe first item[4, 5]restthe rest
Line 6 unpacks prices. first gets the first item, 3, and ...rest collects what is left into a new array, [4, 5].

ES2015 also added modules: files that share values with export and import. This example has a second file, prices.js, which exports tax and withTax.

Importing from another filePop out in the code editor (opens in a new tab)JavaScript
import { tax, withTax } from "./prices.js";
console.log(tax);
console.log(withTax(3));

Line 1 imports two names from prices.js. Line 2 prints 1, and line 3 prints 4, the price plus tax. Press Edit & run to see both files.

Map, Set and a promisePop out in the code editor (opens in a new tab)JavaScript
const menu = new Map([["tea", 3], ["cake", 5]]);
const seen = new Set(["tea", "tea", "cake"]);
console.log(menu.get("cake"), seen.size);
const ready = new Promise((resolve) => setTimeout(() => resolve("tea"), 10));
ready.then((drink) => console.log(drink + " is ready"));
console.log("ordered");

Line 1 makes a Map, a list of keys and values. Line 2 makes a Set, which keeps each value once, so its size is 2. Line 3 prints 5 2.

Line 4 makes a promise, a value that will be ready later: it gives "tea" after 10 milliseconds. Line 5 says what to do when it is ready. Line 6 runs first and prints ordered, then the promise prints tea is ready.

ES2016 to ES2020: async code and safer access

The next four editions each added a few practical tools.

One feature or two from each of ES2016 to ES2019Pop out in the code editor (opens in a new tab)JavaScript
console.log(2 ** 3);
console.log(["tea", "cake"].includes("cake"));
console.log(Object.entries({ tea: 3, cake: 4 }).join(" | "));
console.log("7".padStart(3, "0"));
const order = { item: "tea", price: 3 };
const copy = { ...order, price: 4 };
console.log(copy.price, order.price);
console.log([3, [4, [5]]].flat(Infinity).join(" "));

Lines 1 and 2 are from ES2016: 2 ** 3 is 2 to the power of 3, which prints 8, and includes prints true. Lines 3 and 4 are from ES2017: Object.entries turns an object into pairs, and padStart prints 007.

Line 6 is from ES2018: ...order copies the order's properties into a new object, and price: 4 replaces one. Line 7 prints 4 3, because the original is unchanged. Line 8 is from ES2019: flat(Infinity) prints 3 4 5.

ES2017 also added the biggest change of these years: async functions. Inside one, await pauses the function until a promise is ready, without stopping the rest of the page.

async and awaitPop out in the code editor (opens in a new tab)JavaScript
function wait(ms, value) {
  return new Promise((resolve) => setTimeout(() => resolve(value), ms));
}
async function brew() {
  const tea = await wait(10, "tea");
  const cake = await wait(10, "cake");
  return tea + " and " + cake;
}
brew().then((result) => console.log(result));
console.log("waiting");

Lines 1 to 3 make wait, which gives a value after some milliseconds. Lines 4 to 8 make brew, an async function. Line 5 waits for the tea, and line 6 waits for the cake.

Line 9 starts brew. While it waits, line 10 runs and prints waiting. About 20 milliseconds later, brew finishes and prints tea and cake.

ES2020 added two operators that make reading data safer: ?., optional chaining, and ??, the nullish coalescing operator.

?., ?? and BigIntPop out in the code editor (opens in a new tab)JavaScript
const order = { item: "tea", customer: null, discount: 0 };
console.log(order.customer?.name);
console.log(order.customer?.name ?? "guest");
console.log(order.discount || 10, order.discount ?? 10);
console.log(2n ** 64n);

Line 2 reads order.customer?.name. The customer is null, so ?. stops and gives undefined instead of throwing. Line 3 adds ?? "guest", which uses "guest" when the left side is null or undefined.

Line 4 shows the difference from ||. The discount is 0. || treats 0 as false and gives 10, but ?? keeps 0, so it prints 10 0. Line 5 uses BigInt, whole numbers of any size, and prints 18446744073709551616n.

Four cards in a row with arrows between them: order.customer, then null, then ?. gives undefined, then ?? gives "guest".order.customernullundefined?.??"guest"stops, no TypeErrorthe fallback
On line 2, order.customer is null, so ?. stops and gives undefined instead of throwing. ?? sees undefined and uses "guest".
?. and ?? with and without a customerPop out in the code editor (opens in a new tab)JavaScript
const order = { item: "tea", customer: null };
const name = order.customer?.name ?? "guest";
console.log(name);
const other = { customer: { name: "Asha" } };
console.log(other.customer?.name ?? "guest");

Line 2 stores "guest", because the order has no customer, and line 3 prints it. Line 5 reads a real customer, so ?? keeps the name and it prints Asha.

Step through ?. and ??
Step 0 of 6Ready
Your turn: follow the blue line

A replay of the example code, not a view inside a JavaScript engine. The example runs for real.

Running in
  1. script
Next: line 1
const name = order.customer?.name ?? "guest";console.log(name);const other = { customer: { name: "Asha" } };console.log(other.customer?.name ?? "guest");
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Press Step to see ?. stop at null on line 2, and ?? choose. This replays the example code; it isn't a view inside the JavaScript engine.

ES2021 to ES2024: smaller, sharper tools

These editions filled gaps that people had worked around with helpers for years.

Small tools from ES2021, ES2022 and ES2023Pop out in the code editor (opens in a new tab)JavaScript
const prices = [5, 3, 4];
console.log("tea-tea".replaceAll("tea", "cake"));
let stock = null;
stock ??= 10;
console.log(stock, 1_000_000);
console.log(prices.at(-1), Object.hasOwn({ tea: 3 }, "tea"));
const sorted = prices.toSorted((a, b) => a - b);
console.log(sorted.join(" ") + " / " + prices.join(" "));
console.log(prices.findLast((p) => p > 3));

Line 2 uses replaceAll and prints cake-cake. Lines 3 and 4 use ??=, which gives stock a value only if it is null or undefined. Line 5 prints 10 1000000: the underscores in 1_000_000 are only for reading. All of these are from ES2021.

Line 6 is from ES2022: at(-1) reads the last item, 4, and Object.hasOwn checks for an own property, so it prints 4 true.

Lines 7 to 9 are from ES2023. toSorted makes a sorted copy and leaves the original alone, so line 8 prints 3 4 5 / 5 3 4. Line 9's findLast searches from the end and prints 4.

ES2022 also finished class features: fields written straight in the class body, and private fields, whose names start with # and can be used only inside the class.

Class fields, private fields and a static fieldPop out in the code editor (opens in a new tab)JavaScript
class Cart {
  #items = [];
  static count = 0;
  add(item) {
    this.#items.push(item);
    Cart.count += 1;
    return this;
  }
  get size() {
    return this.#items.length;
  }
}
const cart = new Cart().add("tea").add("cake");
console.log(cart.size, Cart.count, cart.items);

Line 2 makes a private field, #items. Line 3 makes a static field, which belongs to the class itself, not to each cart. Lines 4 to 8 add an item and count it, and line 7 returns the cart so calls can be chained, as on line 13.

Line 14 prints 2 2 undefined. The getter on lines 9 to 11 can read #items, but cart.items finds nothing, because private fields aren't normal properties.

Object.groupBy and Promise.withResolversPop out in the code editor (opens in a new tab)JavaScript
const menu = [
  { name: "tea", kind: "drink" },
  { name: "cake", kind: "food" },
  { name: "juice", kind: "drink" },
];
const groups = Object.groupBy(menu, (item) => item.kind);
console.log(groups.drink.map((item) => item.name).join(", "));
const { promise, resolve } = Promise.withResolvers();
promise.then((value) => console.log("ready: " + value));
resolve("tea");

Line 6, from ES2024, groups the menu by kind. Line 7 prints the drinks: tea, juice.

Line 8 uses Promise.withResolvers, also ES2024, which gives a promise together with the function that completes it. Line 10 calls resolve, so the promise prints ready: tea.

ES2025 and ES2026: the newest editions

ES2025 gave Set the methods people had wanted for years, and added iterator helpers: map, filter, take and more, for any iterator, not just arrays.

Set methods and iterator helpersPop out in the code editor (opens in a new tab)JavaScript
const tea = new Set(["mint", "lemon", "ginger"]);
const cake = new Set(["lemon", "chocolate"]);
console.log([...tea.intersection(cake)].join(", "));
console.log(tea.union(cake).size);
const doubled = [3, 4, 5, 6].values().map((p) => p * 2).take(2).toArray();
console.log(doubled.join(" "));

Lines 1 and 2 make two sets of flavors. Line 3 prints the flavor in both, lemon. Line 4 prints 4, the number of flavors in either.

Line 5 asks the array for an iterator with values(), doubles each item, takes the first two, and turns them into an array. Line 6 prints 6 8.

Unlike array methods, iterator helpers are lazy: they don't touch an item until something asks for it.

A lazy iterator stops earlyPop out in the code editor (opens in a new tab)JavaScript
const prices = [3, 4, 5, 6];
const checked = [];
const first = prices
  .values()
  .filter((p) => {
    checked.push(p);
    return p < 5;
  })
  .map((p) => p * 2)
  .take(1)
  .toArray();
console.log(first.join(" "), "checked:", checked.join(" "));

Lines 3 to 11 describe the steps: filter the prices under 5, double them, and take one. Line 6 records every price that filter checks.

Line 12 prints 6 checked: 3. The first price, 3, passed, and take(1) needed only one, so 4, 5 and 6 were never checked. With an array's filter, all four would have been checked.

Four cards 3, 4, 5 and 6 in a row. An arrow from 3 goes to a card filter, then to map, giving 6, then to take(1), labeled done. Under 4, 5 and 6: never checked.3456never checkedfiltermap: 6take(1)done
The iterator hands out 3 first. It passes filter, map doubles it to 6, and take(1) has all it needs. So 4, 5 and 6 are never checked, and line 12 prints 6 checked: 3.
Step through a lazy iterator
Step 0 of 5Ready
Your turn: follow the blue line

A replay of the example code, not a view inside a JavaScript engine. The iterator helpers run for real, so this needs a browser from 2025 or later.

Running in
  1. script
Next: line 1
const checked = [];const first = prices  .values()  .filter((p) => {    checked.push(p);    return p < 5;  })  .map((p) => p * 2)  .take(1)  .toArray();console.log(first.join(" "), "checked:", checked.join(" "));
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Press Step and watch the checked list beside line 2. It ends with just one price. This replays the example code; it isn't a view inside the JavaScript engine.

RegExp.escape and Promise.tryPop out in the code editor (opens in a new tab)JavaScript
const price = "1.5$";
console.log(new RegExp(price).test("1x5"));
console.log(new RegExp(RegExp.escape(price)).test("1x5"));
console.log(new RegExp(RegExp.escape(price)).test("costs 1.5$"));
const parsed = Promise.try(() => JSON.parse("{bad"));
console.log(parsed instanceof Promise);
parsed.catch((error) => console.log(error.name));

Line 2 builds a regex straight from the text 1.5$. In a regex, . means any character, so it matches 1x5 by mistake and prints true. RegExp.escape makes every special character plain, so line 3 prints false and line 4 prints true.

Line 5 uses Promise.try. The function throws, but instead of a thrown error you get a promise that has failed. Line 6 prints true, and line 7 then prints SyntaxError.

ES2026, the newest edition, came out in June 2026.

Four ES2026 additionsPop out in the code editor (opens in a new tab)JavaScript
const stock = new Map();
stock.getOrInsert("tea", 0);
stock.set("tea", stock.get("tea") + 3);
console.log(stock.getOrInsert("tea", 0));
console.log(Math.sumPrecise([0.1, 0.2, 0.3]), 0.1 + 0.2 + 0.3);
console.log(Error.isError(new TypeError("no tea")), Error.isError({ message: "fake" }));
console.log(new Uint8Array([116, 101, 97]).toBase64());

Line 2 uses getOrInsert: tea isn't in the map, so it stores 0. Line 3 adds 3. On line 4, tea is already there, so getOrInsert just gives it back, and it prints 3.

Line 5 prints 0.6 0.6000000000000001: Math.sumPrecise adds the numbers without the tiny rounding error of +. Line 6 prints true false, because only a real error object passes Error.isError. Line 7 turns three bytes into base64 and prints dGVh.

For what is coming after ES2026, the how JavaScript evolves lesson shows where to follow proposals.

Rewriting old code with modern features

Most codebases mix old and new code. Modern features don't make old code wrong. They let you write the same thing in fewer words, with fewer chances for mistakes. Change old code when you are already working on it, and check that it still gives the same result.

The playground runs the old way and the modern way of doing the same job. Pick a feature, guess whether both print the same thing, then read the two programs line by line.

Playground: the old way and the modern way
const order = { item: "tea", price: 3, customer: null };
console.log("A " + order.item + " costs " + order.price + ".");
The modern way, with template literalPop out in the code editor (opens in a new tab)JavaScript
const order = { item: "tea", price: 3, customer: null };
console.log(`A ${order.item} costs ${order.price}.`);
What each program printssame
oldA tea costs 3.
modernA tea costs 3.
Try it yourself
Same result.

Both programs print the same thing. The template literal version, from ES2015, says it in fewer words.

Both programs run for real in this browser.

Every pair prints the same result. Look at toSorted: the old way needs slice() first to avoid changing the original. And ?. and ?? replace two checks with two short operators.

Before you use a newer feature, check that the browsers your users have support it. The Baseline section of the previous lesson explains how.

Which edition added it?
  • (p) => p * 2
  • class Cart {}
  • new Promise(…)
  • async function brew() {}
  • order.customer?.name
  • 2 ** 3
  • "a-a".replaceAll("a", "b")
  • #items = [] in a class
  • prices.toSorted()
  • Object.groupBy(menu, fn)
  • tea.intersection(cake)
  • [3, 4].values().map(fn)
  • stock.getOrInsert("tea", 0)
Try it yourself
0 of 13 correct

Put each feature with the editions that added it: ES2015, ES2016 to ES2020, ES2021 to ES2024, or ES2025 and ES2026.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

Most mix-ups come from features that look alike but behave differently.

  • "?? is the same as ||." No. ?? falls back only for null and undefined. || also falls back for 0, "" and false.
  • "toSorted sorts the array." No. It returns a sorted copy. sort changes the array itself.
  • "const makes a value unchangeable." No. It stops the variable getting a new value. An object in a const can still change.
  • "await stops the whole page." No. It pauses only its own async function. Other code keeps running.
  • "Private fields are just properties starting with #." No. They are not properties at all, and code outside the class can't reach them.
  • "Iterator helpers work like array methods." No. They are lazy, and handle one item at a time, only when asked.

The table below puts three of these side by side. Read each row as: "people often think this, but this is true."

What is true about three modern features
The featureWhat people thinkWhat is true
??It works like ||.It falls back only for null and undefined, so 0 and "" are kept.
toSortedIt sorts the array.It returns a sorted copy. The original array stays as it was.
ES2015 featuresThey are too new for real projects.Every current browser has supported them for years.

Practice exercises

For each program, find the modern feature and ask what it does differently from the older way. Guess first, then run it.

Exercise 1 · Warm-upPredict ?., ?? and ||

What does line 2 print?

Starter codePop out in the code editor (opens in a new tab)JavaScript
const order = { item: "tea" };
console.log(order.customer?.name ?? "guest", 0 ?? 5, 0 || 5);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upPredict destructuring with a hole and a default

    What does line 2 print?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const [first, , third = 9, ...rest] = [3, 4];
    console.log(first, third, rest.length);

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeFind the bug: the original prices changed

      What does line 3 print? Then fix line 2 so prices keeps its original order.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const prices = [5, 3, 4];
      const sorted = prices.sort((a, b) => a - b);
      console.log(prices.join(" ") + " / " + sorted.join(" "));

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticePredict the order around an await

        What does the program print, in order?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        async function brew() {
          console.log("start");
          await null;
          console.log("after await");
        }
        brew();
        console.log("outside");

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeRewrite an ES5 loop with modern features

          Rewrite the program with const, filter, map, destructuring and a template literal. What does it print?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          var orders = [{ item: "tea", price: 3 }, { item: "cake", price: 5 }, { item: "juice", price: 4 }];
          var names = [];
          for (var i = 0; i < orders.length; i++) {
            if (orders[i].price < 5) names.push(orders[i].item.toUpperCase());
          }
          console.log("Cheap: " + names.join(", "));

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeUse the ES2025 Set methods

            Replace lines 3 and 4 with ES2025 Set methods. What does your program print?

            Starter codePop out in the code editor (opens in a new tab)JavaScript
            const tea = new Set(["mint", "lemon", "ginger"]);
            const cake = new Set(["lemon", "chocolate"]);
            const both = [...tea].filter((flavor) => cake.has(flavor));
            const one = [...tea].filter((flavor) => !cake.has(flavor)).concat([...cake].filter((flavor) => !tea.has(flavor)));
            console.log("both: " + both.join(", ") + " / one: " + one.join(", "));

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, ask which edition the feature came from, and what it does differently from the older way. Each answer explains why it's right or wrong.

              The modern JavaScript quiz · 7 questionsScore: first tries count
              1. Question 1 of 7Which edition added the most since ES5?

                Choose an answer to see the explanation.

              2. Question 2 of 7What do ?? and || give for 0?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                console.log(0 ?? 5, 0 || 5);

                Choose an answer to see the explanation.

              3. Question 3 of 7What does toSorted leave in the original array?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const prices = [5, 3, 4];
                prices.toSorted((a, b) => a - b);
                console.log(prices.join(" "));

                Choose an answer to see the explanation.

              4. Question 4 of 7Which edition brought async functions and await?

                Choose an answer to see the explanation.

              5. Question 5 of 7Which prices does a lazy filter check before take(1) stops it?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const checked = [];
                [3, 4, 5].values().filter((p) => {
                  checked.push(p);
                  return p > 3;
                }).take(1).toArray();
                console.log(checked.join(" "));

                Choose an answer to see the explanation.

              6. Question 6 of 7What does reading a private field by its name print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                class Cup {
                  #size = 3;
                  get size() {
                    return this.#size;
                  }
                }
                const cup = new Cup();
                console.log(cup.size, cup.size === cup["#size"]);

                Choose an answer to see the explanation.

              7. Question 7 of 7Which ES2025 method finds the items two sets have in common?

                Choose an answer to see the explanation.

              Key takeaways

              Modern JavaScript is the big ES2015 foundation plus a small set of tools each year. Knowing which year a feature came from helps you judge whether your users' browsers have it.

              • ES2015 added let, const, arrow functions, classes, modules, promises, destructuring and template literals.
              • ES2017 added async and await. ES2020 added ?. and ??.
              • ?? falls back only for null and undefined; || falls back for any falsy value.
              • ES2022 added private # fields. ES2023 added toSorted, which leaves the original array alone.
              • ES2024 added Object.groupBy. ES2025 added Set methods and lazy iterator helpers.
              • ES2026 added getOrInsert, Math.sumPrecise and Error.isError.
              • Modernize code when you are working on it, and check it still prints the same result.

              Remember the one-liner.
              ES2015 built the foundation, and each June since adds a few sharper tools that work next to all the old ones.

              Coming next: On the horizon. It looks at proposals worth watching, such as decorators and pattern matching, and how to try them today.

              CompleteFrontend Clear concepts. Working examples.