Modern JavaScript Features — ES2020 to ES2026 — Optional Chaining, Nullish Coalescing, Records, and More
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
Convert deep object access checks to optional chaining.
Rewrite a function that takes multiple defaults using ?? instead of ||.
Use .at() to implement a circular buffer (wrap around with modulo).
Use Promise.withResolvers to convert a stream into a promise-based API.
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