Skip to content
elephantoo

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.