Skip to content

Modern JavaScript features

Lesson 24 of 34 14 min read

Optional chaining, nullish coalescing, logical assignment, at(), Object.groupBy and more.


JavaScript has changed enormously since 2015 (ES6). A new edition of the standard ships every year, each adding small features that remove boilerplate and whole categories of bugs. You've already met many — let/const, arrows, classes, modules, destructuring. This lesson is a tour of the everyday modern features you'll see in any current codebase, grouped by the problem they solve.

Safely reading nested data: optional chaining ?.#

Data from APIs often has missing pieces. Before ?., reading deep properties needed long guard chains:

JavaScript
const order = { id: 1, customer: { name: "Ada" } };

// Old way
const cityOld = order && order.customer && order.customer.address && order.customer.address.city;

// Modern way
const city = order.customer?.address?.city;
console.log(cityOld, city); // undefined undefined

?. short-circuits: if the value on its left is null or undefined, the whole chain returns undefined instead of throwing. It works for methods and brackets too:

JavaScript
const api = { getUser: () => ({ name: "Ada" }) };
console.log(api.getUser?.().name);  // Ada
console.log(api.deleteUser?.());    // undefined – method doesn't exist, not called

const list = null;
console.log(list?.[0]);             // undefined

Use ?. where a value can legitimately be missing. Sprinkling it everywhere hides real bugs — if order should always exist, let order.id fail loudly.

Better defaults: nullish coalescing ??#

|| falls back for any falsy value, which wrongly replaces 0, "" and false:

JavaScript
const settings = { volume: 0, nickname: "", notifications: false };

console.log(settings.volume || 50);        // 50 ❌ the user chose 0
console.log(settings.volume ?? 50);        // 0  ✅
console.log(settings.nickname ?? "Guest"); // "" ✅ (empty but deliberate)
console.log(settings.theme ?? "light");    // light – missing, so default

?? only falls back for null and undefined. Combine it with ?.:

JavaScript
const response = { data: { items: [] } };
const firstTitle = response.data?.items?.[0]?.title ?? "No items yet";
console.log(firstTitle); // No items yet

Logical assignment: ??=, ||=, &&=#

JavaScript
const config = { retries: 0, timeout: null, debug: true };

config.retries ??= 3;    // stays 0 – it isn't null/undefined
config.timeout ??= 5000; // 5000 – was null
config.label ||= "default"; // assigned because undefined is falsy
config.debug &&= "verbose"; // assigned only because debug was truthy

console.log(config); // { retries: 0, timeout: 5000, debug: 'verbose', label: 'default' }

A common idiom: lazily initialise a cache entry.

JavaScript
const cache = {};
function getBucket(key) {
  return (cache[key] ??= []);
}
getBucket("js").push("closures");
getBucket("js").push("promises");
console.log(cache); // { js: [ 'closures', 'promises' ] }

Relative indexing: at()#

JavaScript
const lessons = ["intro", "variables", "functions"];
console.log(lessons.at(0));  // intro
console.log(lessons.at(-1)); // functions – instead of lessons[lessons.length - 1]
console.log("hello".at(-2)); // l

Non-mutating array methods (ES2023)#

JavaScript
const scores = [72, 95, 88];

const sorted = scores.toSorted((a, b) => b - a);
const reversed = scores.toReversed();
const updated = scores.with(1, 100);
const withoutFirst = scores.toSpliced(0, 1);

console.log(scores);   // [ 72, 95, 88 ] – unchanged
console.log(sorted, reversed, updated, withoutFirst);
// [ 95, 88, 72 ] [ 88, 95, 72 ] [ 72, 100, 88 ] [ 95, 88 ]

console.log(scores.findLast((s) => s > 80)); // 88
console.log(scores.findLastIndex((s) => s > 80)); // 2

These are perfect for React state and any code that values immutability.

Grouping: Object.groupBy and Map.groupBy (ES2024)#

JavaScript
const people = [
  { name: "Ada", team: "core" },
  { name: "Alan", team: "infra" },
  { name: "Grace", team: "core" },
];

const byTeam = Object.groupBy(people, (p) => p.team);
console.log(byTeam.core.map((p) => p.name)); // [ 'Ada', 'Grace' ]

Easier object helpers#

JavaScript
const product = { id: 7, price: 499 };

console.log(Object.hasOwn(product, "price")); // true – replaces hasOwnProperty

const copy = structuredClone({ nested: { deep: [1, 2] } }); // deep copy
console.log(copy.nested.deep); // [ 1, 2 ]

const entries = Object.entries(product).map(([k, v]) => [k.toUpperCase(), v]);
console.log(Object.fromEntries(entries)); // { ID: 7, PRICE: 499 }

String improvements#

JavaScript
console.log("a-b-c".replaceAll("-", "+")); // a+b+c
console.log("5".padStart(3, "0"));         // 005
console.log("  trim me ".trimEnd() + "|"); //   trim me|
console.log(`Total: ${2 + 3}`);            // Total: 5

Numbers#

JavaScript
const population = 1_400_000_000;   // numeric separators for readability
console.log(population);             // 1400000000

const big = 2n ** 64n;               // BigInt for integers beyond 2^53
console.log(big);                    // 18446744073709551616n

console.log(Number.isInteger(5.0), Number.isSafeInteger(2 ** 53)); // true false
console.log(2 ** 10);                // 1024 – exponent operator

Classes: fields, private members and static blocks#

JavaScript
class Config {
  static #instance;
  static {
    // static initialisation block – runs once when the class is defined
    Config.#instance = new Config();
  }
  static get instance() {
    return Config.#instance;
  }

  #secret = "hidden";
  env = "production";

  hasSecret() {
    return #secret in this; // ergonomic brand check (ES2022)
  }
}

console.log(Config.instance.env, Config.instance.hasSecret()); // production true

Asynchronous features#

You'll study these properly in the async module:

JavaScript
// Promise.withResolvers (ES2024): create a promise and its controls together
const { promise, resolve } = Promise.withResolvers();
setTimeout(() => resolve("done"), 10);
console.log(await promise); // done (top-level await in a module)

// Promise.allSettled / Promise.any
const results = await Promise.allSettled([Promise.resolve(1), Promise.reject(new Error("x"))]);
console.log(results.map((r) => r.status)); // [ 'fulfilled', 'rejected' ]

// Array.fromAsync: collect an async iterable into an array
async function* countdown() {
  yield 3;
  yield 2;
  yield 1;
}
console.log(await Array.fromAsync(countdown())); // [ 3, 2, 1 ]

(Save this as an .mjs file to run it with Node — top-level await only works in modules.)

Iterator helpers and Set methods (ES2025)#

The newest engines (Node 22+, current Chrome, Firefox and Safari) add lazy helpers to iterators and mathematical methods to Sets:

JavaScript
function* naturals() {
  let n = 1;
  while (true) yield n++;
}

const firstEvenSquares = naturals()
  .filter((n) => n % 2 === 0)
  .map((n) => n * n)
  .take(3)
  .toArray();
console.log(firstEvenSquares); // [ 4, 16, 36 ]

const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);
console.log([...a.intersection(b)]); // [ 2, 3 ]
console.log([...a.union(b)]);        // [ 1, 2, 3, 4 ]

Iterator helpers are lazy: even though naturals() is infinite, only as many values as needed are computed.

Escaping text for regexes: RegExp.escape (ES2025)#

JavaScript
const userInput = "1+1=2?";
const re = new RegExp(RegExp.escape(userInput));
console.log(re.test("Is 1+1=2? Yes")); // true

How do I know what I can use?#

  • Browsers: check caniuse.com or MDN's compatibility tables. Everything up to ES2023 is supported in all current browsers.
  • Node.js: use a current LTS (Node 24 at the time of writing) — node.green lists feature support.
  • Build tools like Vite and TypeScript can compile newer syntax down for older targets, but they don't add missing built-in methods (like Object.groupBy) — those need a polyfill (e.g. core-js) if you support old browsers.

Common mistakes#

  • Using || for defaults when 0, "" or false are valid values.
  • Overusing ?. and hiding genuine bugs.
  • Mixing ?? with || or && without parentheses — a ?? b || c is a SyntaxError; write (a ?? b) || c.
  • Assuming the newest features (ES2025) exist in every environment your users have.

What's next#

You've completed the language deep-dive. Next comes one of the most important topics in JavaScript: asynchronous code, starting with the event loop.

Check your understanding

Quick quiz

0/3 answered
  1. 1.What does user?.profile?.avatar evaluate to when user.profile is undefined?

  2. 2.After let count = 0; count ||= 10;, what is count?

  3. 3.What does ["a", "b", "c"].at(-1) return?

Finished reading?

Mark this lesson complete to track your progress.