Skip to content

Modern JavaScript Features — ES2020 to ES2026 — Optional Chaining, Nullish Coalescing, Records, and More

DodaTech Updated 2026-06-29 6 min read

In this tutorial, you will learn about Modern JavaScript Features. We cover key concepts, practical examples, and best practices to help you master this topic.

JavaScript evolves rapidly. Each year brings new features that make code cleaner, safer, and more expressive. From optional chaining (ES2020) to records/tuples (ES2026), staying current with the language reduces boilerplate and eliminates entire categories of bugs.

DodaTech targets modern JavaScript runtimes (Node 22+, Chrome 120+, Firefox 120+) and uses these features throughout the codebase.

What You'll Learn

  • ES2020: optional chaining, nullish coalescing, globalThis
  • ES2021: Promise.any, String.replaceAll, numeric separators
  • ES2022: .at(), RegExp match indices, error.cause
  • ES2023: toSorted, toReversed, findLast
  • ES2024: Promise.withResolvers, Map.groupBy
  • ES2025-2026: Records, Tuples, Temporal (Stage 3+)

ES2020 Features

Optional Chaining (?.)

// Before: nested null checks
const city = user && user.address && user.address.city;
const zip = user ? user.address ? user.address.zip : null : null;

// After: optional chaining
const city = user?.address?.city;
const zip = user?.address?.zip;

// Also works with function calls
const result = obj.method?.(); // Only call if method exists

// And dynamic access
const value = obj?.[key]; // Only access if obj is not null/undefined

Nullish Coalescing (??)

// Before: || treats "" and 0 as falsy
const name = input.name || "default";     // Wrong if name is ""
const count = input.count || 10;          // Wrong if count is 0

// After: ?? only replaces null/undefined
const name = input.name ?? "default";     // Keeps "" 
const count = input.count ?? 10;          // Keeps 0

// Combine with optional chaining
const score = user?.scores?.[0] ?? 0;

globalThis

// Before:
const g = typeof window !== "undefined" ? window
        : typeof global !== "undefined" ? global
        : typeof self !== "undefined" ? self
        : {};

// After:
console.log(globalThis); // Works in browser, Node, Workers, Deno

ES2021 Features

Promise.any

// Resolves with the first fulfilled promise
const promises = [
  fetch("/slow-server").then(r => r.json()),
  fetch("/fast-cdn/data.json").then(r => r.json()),
];

Promise.any(promises)
  .then(result => console.log("First success:", result))
  .catch(err => console.error("All failed:", err.errors));

// AggregateError contains all rejection reasons

String.replaceAll

// Before: regex with global flag
"a-b-c".replace(/-/g, "_");   // "a_b_c"

// After: replaceAll
"a-b-c".replaceAll("-", "_"); // "a_b_c"

// With function
"user:alice, role:admin".replaceAll(
  /(\w+):(\w+)/g,
  (match, key, value) => `${key}=${value}`
);

Numeric Separators

const billion = 1_000_000_000;  // 1 billion
const bytes = 0xFF_EC_DE_5E;    // Hex with byte separators
const binary = 0b1010_0001;     // Binary groups
const tiny = 1e-6;              // Works with exponents

ES2022 Features

.at() Method

const arr = [10, 20, 30, 40];

// Before: accessing last element
arr[arr.length - 1]; // 40

// After: negative indexing with .at()
arr.at(-1);  // 40
arr.at(-2);  // 30

// Also on strings:
"hello".at(-1); // "o"

Object.hasOwn

const obj = { name: "Alice" };

// Before:
obj.hasOwnProperty("name");   // true
Object.prototype.hasOwnProperty.call(obj, "name"); // Safe version

// After:
Object.hasOwn(obj, "name");   // true
Object.hasOwn(obj, "toString"); // false (not own)

Error Cause

async function fetchData(id) {
  try {
    const response = await fetch(`/api/${id}`);
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`, {
        cause: { status: response.status, statusText: response.statusText }
      });
    }
    return response.json();
  } catch (err) {
    throw new Error(`Failed to fetch ${id}`, { cause: err });
  }
}

// Usage:
try {
  await fetchData(42);
} catch (err) {
  console.log(err.message);        // "Failed to fetch 42"
  console.log(err.cause.message);  // "HTTP 404"
  console.log(err.cause.cause);    // { status: 404, ... }
}

ES2023 Features

Immutable Array Methods

const arr = [3, 1, 4, 1, 5];

// toSorted() — returns new sorted array
const sorted = arr.toSorted();        // [1, 1, 3, 4, 5]
console.log(arr);                      // [3, 1, 4, 1, 5] (unchanged)

// toReversed()
const reversed = arr.toReversed();     // [5, 1, 4, 1, 3]

// toSpliced()
const spliced = arr.toSpliced(1, 2);  // [3, 1, 5]

// with() — replace element at index
const withNine = arr.with(2, 9);      // [3, 1, 9, 1, 5]

findLast / findLastIndex

const numbers = [1, 2, 3, 4, 5, 6];

// Find last even number
numbers.findLast(n => n % 2 === 0);       // 6
numbers.findLastIndex(n => n % 2 === 0);  // 5

// Useful for reverse searching
const users = [
  { id: 1, active: false },
  { id: 2, active: true },
  { id: 3, active: true },
];
users.findLast(u => u.active); // { id: 3, active: true }

ES2024 Features

Promise.withResolvers

// Before: wrapping in new Promise
function makePromise() {
  let resolve, reject;
  const promise = new Promise((res, rej) => {
    resolve = res;
    reject = rej;
  });
  return { promise, resolve, reject };
}

// After:
const { promise, resolve, reject } = Promise.withResolvers();

// Useful for callback-based APIs:
function readFile(path) {
  const { promise, resolve, reject } = Promise.withResolvers();
  fs.readFile(path, "utf-8", (err, data) => {
    if (err) reject(err);
    else resolve(data);
  });
  return promise;
}

Map.groupBy

const items = [
  { category: "a", value: 1 },
  { category: "b", value: 2 },
  { category: "a", value: 3 },
];

const grouped = Map.groupBy(items, item => item.category);
// Map { "a" => [{ category: "a", value: 1 }, { category: "a", value: 3 }],
//         "b" => [{ category: "b", value: 2 }] }

ES2025-2026 Proposals (Stage 3+)

Records and Tuples (Immutable Data)

// Proposals — syntax may change
// Records (immutable objects):
const user = #{
  name: "Alice",
  age: 30,
  address: #{
    city: "NYC",
    zip: "10001"
  }
};

// Tuples (immutable arrays):
const scores = #[95, 88, 92];

// Deep equality:
const a = #{ x: 1, y: 2 };
const b = #{ x: 1, y: 2 };
console.log(a === b); // true (deep equality!)

// Useful for Redux state, memoization, React props

Best Practices

// 1. Prefer ?? over || for default values
const port = config.port ?? 3000;

// 2. Use optional chaining over long && chains
const name = response?.user?.profile?.name;

// 3. Use .at(-1) for last element
const last = arr.at(-1);

// 4. Use toSorted() for immutable sorts
const sorted = [...arr].sort();  // Old
const sorted = arr.toSorted();   // New

// 5. Use nullish coalescing assignment
user.count ??= 0;  // Only assigns if null/undefined

Practice Questions

  1. Convert deep object access checks to optional chaining.

  2. Rewrite a function that takes multiple defaults using ?? instead of ||.

  3. Use .at() to implement a circular buffer (wrap around with modulo).

  4. Use Promise.withResolvers to convert a stream into a promise-based API.

  5. Use Array.with() to implement immutable state updates (like Redux reducer).

Challenge: Deep Defaults Resolver

Write a function that merges an options object with defaults using ?? semantics (not ||):

function resolveDefaults(options, defaults) {
  // Should handle nested objects
  // Only replace null/undefined, not "" or 0 or false
}

const defaults = {
  host: "localhost",
  port: 3000,
  headers: { timeout: 5000, retries: 3 }
};

const result = resolveDefaults(
  { port: 0, headers: { retries: 0 } },
  defaults
);
// { host: "localhost", port: 0, headers: { timeout: 5000, retries: 0 } }

This is the pattern DodaTech uses for merging user configuration with system defaults in security scan settings.

Real-World Task: Configuration Normalizer

Build a function that normalizes user-provided configuration:

  • Use ?? for default values (preserve 0, false, "")
  • Use optional chaining for nested access
  • Validate and coerce types
  • Report warnings for unknown keys
  • Use error cause for validation errors

This powers DodaTech's configuration system where users provide partial configuration and the system fills in secure defaults for every scan profile.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro