DB
ALPHA
Photos

JavaScript’s Game-Changing Features Since 2020: A Developer’s Guide

JavaScript has experienced an incredible period of innovation since 2020, with each annual release bringing powerful new features that fundamentally change how we write code. From logical assignment operators to top-level await, these additions have made JavaScript more expressive, safer, and more developer-friendly than ever before.

Let’s explore the most impactful features introduced since 2020 and see how they’re reshaping modern JavaScript development.

ES2020 (ES11)

Optional Chaining (?.)

Optional chaining revolutionized how we handle potentially undefined object properties, eliminating the need for verbose null checks.

// Before: Verbose and error-prone
if (
user &&
user.profile &&
user.profile.address &&
user.profile.address.street
) {
console.log(user.profile.address.street);
}
// After: Clean and safe
console.log(user?.profile?.address?.street);
// Works with arrays and function calls too
const firstItem = items?.[0];
const result = api?.getData?.();

Nullish Coalescing (??)

This operator provides a cleaner way to handle null and undefined values, distinguishing them from other falsy values.

// Before: Problematic with falsy values
const port = process.env.PORT || 3000; // Fails if PORT is "0"
// After: Only null/undefined trigger fallback
const port = process.env.PORT ?? 3000; // "0" is preserved
// Perfect for default values
const config = {
theme: userTheme ?? "light",
timeout: userTimeout ?? 5000,
debug: userDebug ?? false,
};

BigInt for Large Numbers

BigInt enables working with integers larger than Number.MAX_SAFE_INTEGER.

// Traditional number limitations
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
console.log(9007199254740992 === 9007199254740993); // true (precision lost!)
// BigInt to the rescue
const hugeNumber = 9007199254740992n;
const anotherHuge = BigInt("9007199254740993");
console.log(hugeNumber === anotherHuge); // false (precision maintained)
// Useful for cryptography, timestamps, and large calculations
const timestamp = BigInt(Date.now()) * 1000000n; // Nanosecond precision

Dynamic Imports

Dynamic imports enable code splitting and lazy loading, crucial for modern web performance.

// Conditional loading
if (shouldLoadChart) {
const { Chart } = await import("./chart.js");
const chart = new Chart(data);
}
// Feature detection loading
const supportsWebGL = checkWebGLSupport();
const renderer = supportsWebGL
? await import("./webgl-renderer.js")
: await import("./canvas-renderer.js");
// Route-based code splitting (common in SPAs)
const routes = {
"/dashboard": () => import("./pages/Dashboard.js"),
"/profile": () => import("./pages/Profile.js"),
"/settings": () => import("./pages/Settings.js"),
};

ES2021 (ES12)

Logical Assignment Operators

These operators combine logical operations with assignment, reducing boilerplate code significantly.

// Nullish assignment (??=)
// Before
if (config.apiUrl == null) {
config.apiUrl = "https://api.example.com";
}
// After
config.apiUrl ??= "https://api.example.com";
// AND assignment (&&=)
// Only assign if the left side is truthy
user.preferences &&= updatePreferences(user.preferences);
// OR assignment (||=)
// Assign if left side is falsy
cache.data ||= fetchExpensiveData();
// Real-world usage
function initializeApp(options = {}) {
options.theme ??= "light";
options.debug &&= process.env.NODE_ENV === "development";
options.plugins ||= [];
return options;
}

String.prototype.replaceAll()

Finally, a straightforward way to replace all occurrences of a substring without regex.

// Before: Regex required for global replacement
const text = "foo bar foo baz foo";
const result1 = text.replace(/foo/g, "qux"); // "qux bar qux baz qux"
// After: Simple and intuitive
const result2 = text.replaceAll("foo", "qux"); // "qux bar qux baz qux"
// Especially useful for special characters
const code = "user?.name?.first";
const sanitized = code.replaceAll("?.", "_optional_"); // No regex escaping needed
// Case-sensitive by default, but regex still works for complex patterns
const caseInsensitive = text.replaceAll(/FOO/gi, "qux");

Numeric Separators

Improve readability of large numbers with underscore separators.

// Large numbers become readable
const million = 1_000_000;
const billion = 1_000_000_000;
const binary = 0b1010_0001_1000_0101;
const hex = 0xff_ec_de_5e;
const bigInt = 123_456n;
// Financial calculations
const budget = 2_500_000.5;
const taxRate = 0.08_5; // 8.5%
// Scientific notation
const lightSpeed = 2.998_792_458e8; // meters per second

ES2022 (ES13)

Top-Level Await

Use await directly in modules without wrapping in async functions.

// Before: Required async wrapper
(async () => {
const config = await fetch("/api/config").then((r) => r.json());
const app = createApp(config);
app.start();
})();
// After: Clean top-level await
const config = await fetch("/api/config").then((r) => r.json());
const app = createApp(config);
app.start();
// Dynamic imports with await
const { default: analytics } = await import("./analytics.js");
await analytics.initialize();
// Conditional module loading
const isDevelopment = process.env.NODE_ENV === "development";
if (isDevelopment) {
await import("./dev-tools.js");
}

Class Fields and Private Methods

Modern class syntax with proper encapsulation.

class User {
// Public fields
name = "";
email = "";
// Private fields (truly private!)
#password = "";
#loginAttempts = 0;
// Static fields
static maxLoginAttempts = 5;
constructor(name, email) {
this.name = name;
this.email = email;
}
// Private methods
#hashPassword(password) {
return crypto.subtle.digest("SHA-256", password);
}
#resetLoginAttempts() {
this.#loginAttempts = 0;
}
// Static private method
static #validateEmail(email) {
return email.includes("@");
}
// Public methods can access private members
async setPassword(password) {
this.#password = await this.#hashPassword(password);
this.#resetLoginAttempts();
}
login(password) {
if (this.#loginAttempts >= User.maxLoginAttempts) {
throw new Error("Too many login attempts");
}
// Login logic using this.#password
this.#loginAttempts++;
}
}
// Private fields are truly inaccessible
const user = new User("John", "john@example.com");
console.log(user.name); // 'John'
console.log(user.#password); // SyntaxError!

Object.hasOwn()

A better alternative to hasOwnProperty that works with all objects.

// Before: hasOwnProperty issues
const obj = Object.create(null); // No prototype
obj.hasOwnProperty("key"); // TypeError!
const dict = { hasOwnProperty: "not a function" };
dict.hasOwnProperty("key"); // TypeError!
// After: Object.hasOwn() always works
Object.hasOwn(obj, "key"); // false
Object.hasOwn(dict, "hasOwnProperty"); // true
// Real-world usage
function processConfig(config) {
const defaults = { theme: "light", timeout: 5000 };
for (const key in defaults) {
if (!Object.hasOwn(config, key)) {
config[key] = defaults[key];
}
}
return config;
}

Array.at() Method

Access array elements with negative indices, just like Python.

const items = ["first", "second", "third", "last"];
// Before: Cumbersome negative indexing
const lastItem = items[items.length - 1]; // 'last'
const secondToLast = items[items.length - 2]; // 'third'
// After: Clean negative indexing
const lastItem = items.at(-1); // 'last'
const secondToLast = items.at(-2); // 'third'
const firstItem = items.at(0); // 'first'
// Works with strings too
const text = "JavaScript";
console.log(text.at(-1)); // 't'
console.log(text.at(-4)); // 'r'
// Perfect for dynamic access
function getRecentItems(arr, count = 3) {
return Array.from({ length: count }, (_, i) => arr.at(-1 - i)).filter(
Boolean
);
}

ES2023 (ES14)

Array Methods: findLast() and findLastIndex()

Search arrays from the end, perfect for recent items or reverse searches.

const logs = [
{ level: "info", message: "App started", timestamp: 1001 },
{ level: "warn", message: "Low memory", timestamp: 1002 },
{ level: "error", message: "Database error", timestamp: 1003 },
{ level: "info", message: "Request processed", timestamp: 1004 },
];
// Find the last error (most recent)
const lastError = logs.findLast((log) => log.level === "error");
console.log(lastError.message); // 'Database error'
// Find index of last warning
const lastWarnIndex = logs.findLastIndex((log) => log.level === "warn");
console.log(lastWarnIndex); // 1
// Real-world usage: Find most recent matching transaction
function findRecentTransaction(transactions, criteria) {
return transactions.findLast(
(tx) => tx.amount >= criteria.minAmount && tx.category === criteria.category
);
}

Array Methods: toSorted(), toReversed(), toSpliced(), with()

Immutable array operations that return new arrays instead of mutating originals.

const original = [3, 1, 4, 1, 5];
// toSorted() - doesn't mutate original
const sorted = original.toSorted((a, b) => a - b);
console.log(original); // [3, 1, 4, 1, 5] (unchanged)
console.log(sorted); // [1, 1, 3, 4, 5]
// toReversed() - immutable reverse
const reversed = original.toReversed();
console.log(reversed); // [5, 1, 4, 1, 3]
// with() - immutable element replacement
const updated = original.with(2, 999);
console.log(updated); // [3, 1, 999, 1, 5]
// toSpliced() - immutable splice
const spliced = original.toSpliced(1, 2, "a", "b");
console.log(spliced); // [3, 'a', 'b', 1, 5]
// Functional programming friendly
const pipeline = [5, 2, 8, 1, 9]
.toSorted((a, b) => b - a) // Sort descending
.with(0, 100) // Replace first element
.toSpliced(2, 1); // Remove third element

ES2024 (ES15)

Object.groupBy() and Map.groupBy()

Native grouping functionality that eliminates the need for external libraries.

const products = [
{ name: "Laptop", category: "Electronics", price: 999 },
{ name: "Shirt", category: "Clothing", price: 29 },
{ name: "Phone", category: "Electronics", price: 699 },
{ name: "Jeans", category: "Clothing", price: 79 },
];
// Group by category
const grouped = Object.groupBy(products, (product) => product.category);
console.log(grouped);
/* {
Electronics: [{ name: 'Laptop', ... }, { name: 'Phone', ... }],
Clothing: [{ name: 'Shirt', ... }, { name: 'Jeans', ... }]
} */
// Group by price range
const byPriceRange = Object.groupBy(products, (product) =>
product.price > 500 ? "expensive" : "affordable"
);
// Map.groupBy for when you need Map features
const mapGrouped = Map.groupBy(products, (product) => product.category);
console.log(mapGrouped.get("Electronics")); // Array of electronics
// Advanced grouping
const users = [
{ name: "Alice", age: 25, department: "Engineering" },
{ name: "Bob", age: 30, department: "Engineering" },
{ name: "Carol", age: 25, department: "Design" },
];
const groupedUsers = Object.groupBy(
users,
(user) => `${user.department}-${Math.floor(user.age / 10) * 10}s`
);
// Groups like 'Engineering-20s', 'Design-20s', etc.

Well-Formed Unicode Strings

Methods to handle and validate Unicode strings properly.

// Check if string is well-formed Unicode
const validString = "Hello 👋 World";
const invalidString = "\uD800"; // Lone surrogate
console.log(validString.isWellFormed()); // true
console.log(invalidString.isWellFormed()); // false
// Convert to well-formed Unicode
console.log(invalidString.toWellFormed()); // "�" (replacement character)
// Useful for data validation
function sanitizeUserInput(input) {
if (!input.isWellFormed()) {
return input.toWellFormed();
}
return input;
}
// Important for international applications
const emoji = "👨‍👩‍👧‍👦"; // Family emoji (complex Unicode)
console.log(emoji.isWellFormed()); // true
console.log([...emoji].length); // 7 (individual Unicode points)

Promise.withResolvers()

A cleaner way to create promises with external resolve/reject functions.

// Before: Creating externally controllable promises
let resolvePromise, rejectPromise;
const promise = new Promise((resolve, reject) => {
resolvePromise = resolve;
rejectPromise = reject;
});
// After: Much cleaner
const { promise, resolve, reject } = Promise.withResolvers();
// Real-world usage: Event-driven promises
function waitForEvent(element, eventType, timeout = 5000) {
const { promise, resolve, reject } = Promise.withResolvers();
const cleanup = () => {
element.removeEventListener(eventType, onEvent);
clearTimeout(timeoutId);
};
const onEvent = (event) => {
cleanup();
resolve(event);
};
const timeoutId = setTimeout(() => {
cleanup();
reject(new Error(`Event ${eventType} timed out`));
}, timeout);
element.addEventListener(eventType, onEvent, { once: true });
return promise;
}
// Usage
const clickEvent = await waitForEvent(button, "click", 3000);