Modern JavaScript: ES2020 to ES2024 Features You Should Be Using
JavaScript evolves every year through the TC39 proposal process. Keeping up with new features makes your code cleaner, shorter, and less error-prone. This guide covers the most practical additions from ES2020 through ES2024 β features that are now safe to use in all modern environments.
ES2020
Optional Chaining (?.)
Access deeply nested properties without long chains of null checks:
javascript-- Before: verbose and error-prone const street = user && user.address && user.address.street; const length = arr && arr.length; const result = obj && obj.method && obj.method(); -- After: clean and safe const street = user?.address?.street; const length = arr?.length; const result = obj?.method?.(); -- Works with bracket notation const tag = post?.tags?.[0]; -- Works with methods const html = element?.innerHTML; document.querySelector(".hero")?.classList.add("visible"); -- Short-circuits to undefined on null/undefined const city = null?.address?.city; -- undefined (no TypeError)
Nullish Coalescing (??)
Provide a default only when a value is null or undefined (not for falsy values like 0 or ""):
javascript-- Problem with ||: treats 0, "", false as falsy const timeout = userTimeout || 3000; -- BUG: 0 becomes 3000 const name = userName || "Anonymous"; -- BUG: "" becomes "Anonymous" -- ?? only triggers on null/undefined const timeout = userTimeout ?? 3000; -- 0 stays 0 const name = userName ?? "Anonymous"; -- "" stays "" const port = config.port ?? 8080; const count = data?.count ?? 0;
Promise.allSettled
Wait for all promises, even if some fail:
javascriptconst results = await Promise.allSettled([ fetch("/api/users"), fetch("/api/posts"), fetch("/api/settings"), ]); results.forEach(result => { if (result.status === "fulfilled") { console.log("Success:", result.value); } else { console.log("Failed:", result.reason); } });
BigInt
Integers larger than Number.MAX_SAFE_INTEGER (2^53 - 1):
javascriptconst big = 9007199254740993n; -- BigInt literal const also = BigInt("9007199254740993"); big + 1n; -- 9007199254740994n big * 2n; -- BigInt typeof big; -- "bigint" -- Cannot mix with regular numbers big + 1; -- TypeError: Cannot mix BigInt and other types Number(big); -- convert to Number (may lose precision)
ES2021
Logical Assignment Operators
Combine logical operators with assignment:
javascript-- Logical OR assignment: assign if left side is falsy user.name ||= "Anonymous"; -- equivalent to: user.name = user.name || "Anonymous" -- Logical AND assignment: assign only if left side is truthy config.debug &&= process.env.NODE_ENV === "development"; -- equivalent to: if (config.debug) config.debug = ... -- Nullish assignment: assign only if left side is null/undefined settings.timeout ??= 3000; -- equivalent to: settings.timeout = settings.timeout ?? 3000 -- Practical: initializing defaults in an object function initConfig(config) { config.retries ??= 3; config.timeout ??= 5000; config.debug ??= false; config.logLevel ||= "info"; return config; }
String replaceAll
javascript-- Before "a_b_c_d".replace(/_/g, "-"); -- needed regex for global replace -- After "a_b_c_d".replaceAll("_", "-"); -- "a-b-c-d" "hello world".replaceAll(" ", "_"); -- "hello_world"
Numeric Separators
javascript-- Underscores as visual separators (ignored by JS engine) const million = 1_000_000; const pi = 3.141_592_653; const bytes = 0xFF_FF_FF_FF; const binary = 0b1010_0001; const credit_card = 4111_1111_1111_1111n;
ES2022
Array.at() and String.at()
Access elements from the end without .length - 1:
javascriptconst arr = [1, 2, 3, 4, 5]; arr.at(0); -- 1 (same as arr[0]) arr.at(-1); -- 5 (last element) arr.at(-2); -- 4 (second to last) "hello".at(-1); -- "o" "hello".at(0); -- "h" -- Before const last = arr[arr.length - 1]; -- After const last = arr.at(-1);
Object.hasOwn
Safer replacement for Object.prototype.hasOwnProperty.call():
javascript-- Before: verbose Object.prototype.hasOwnProperty.call(obj, "key"); -- After: clean Object.hasOwn(obj, "key"); -- Works correctly on objects with null prototype const map = Object.create(null); map.foo = "bar"; Object.hasOwn(map, "foo"); -- true (works) map.hasOwnProperty("foo"); -- TypeError (no prototype!) -- Practical use const config = { port: 3000 }; if (Object.hasOwn(config, "port")) { console.log(config.port); }
Error.cause
Wrap errors with context while preserving the original:
javascriptasync function fetchUser(userId) { try { const response = await fetch(`/api/users/${userId}`); return await response.json(); } catch (error) { throw new Error(`Failed to fetch user ${userId}`, { cause: error }); } } try { await fetchUser(42); } catch (error) { console.error(error.message); -- "Failed to fetch user 42" console.error(error.cause.message); -- original network error }
Class Fields and Private Class Members
javascriptclass BankAccount { -- Public fields owner; currency = "USD"; -- Private fields (truly private, not just convention) #balance = 0; #transactionHistory = []; constructor(owner, initialBalance) { this.owner = owner; this.#balance = initialBalance; } -- Private method #logTransaction(type, amount) { this.#transactionHistory.push({ type, amount, date: new Date() }); } deposit(amount) { if (amount <= 0) throw new Error("Amount must be positive"); this.#balance += amount; this.#logTransaction("deposit", amount); } get balance() { return this.#balance; } } const account = new BankAccount("Alice", 1000); account.deposit(500); console.log(account.balance); -- 1500 console.log(account.#balance); -- SyntaxError: private field console.log(account.#transactionHistory); -- SyntaxError
ES2023
Array findLast and findLastIndex
javascriptconst arr = [1, 3, 5, 7, 5, 3, 1]; arr.findLast(n => n < 5); -- 3 (searches from end) arr.findLastIndex(n => n < 5); -- 5 (index of last match) -- Useful for finding the most recent item matching a condition const logs = [ { level: "info", msg: "Started" }, { level: "error", msg: "Failed" }, { level: "info", msg: "Retrying" }, { level: "error", msg: "Failed again" }, ]; const lastError = logs.findLast(log => log.level === "error"); -- { level: "error", msg: "Failed again" }
Array toSorted, toReversed, toSpliced, with
Non-mutating alternatives to sort, reverse, and splice:
javascriptconst original = [3, 1, 4, 1, 5, 9]; -- Mutates original original.sort(); -- original is now [1, 1, 3, 4, 5, 9] -- Non-mutating versions return new arrays const sorted = original.toSorted(); -- new array, original unchanged const reversed = original.toReversed(); -- new reversed array const spliced = original.toSpliced(2, 1, 99); -- new array with element replaced const updated = original.with(0, 99); -- new array with index 0 = 99 -- Especially useful in React state updates setItems(items.toSorted((a, b) => a.price - b.price)); setItems(items.with(selectedIndex, updatedItem));
structuredClone
Deep clone objects natively β no more JSON.parse(JSON.stringify(...)):
javascript-- Before: limited (loses dates, undefined, functions; circular refs throw) const clone = JSON.parse(JSON.stringify(obj)); -- After: handles dates, RegExp, Map, Set, circular references const original = { name: "Alice", scores: [95, 87, 92], created: new Date(), data: new Map([["key", "value"]]), }; const clone = structuredClone(original); clone.scores.push(100); console.log(original.scores); -- [95, 87, 92] -- unaffected console.log(clone.created instanceof Date); -- true (proper Date, not string)
ES2024
Object.groupBy and Map.groupBy
Group arrays by a key function:
javascriptconst products = [ { name: "Laptop", category: "electronics", price: 999 }, { name: "Phone", category: "electronics", price: 699 }, { name: "T-Shirt", category: "clothing", price: 29 }, { name: "Jeans", category: "clothing", price: 59 }, ]; -- Group into a plain object const byCategory = Object.groupBy(products, p => p.category); -- { -- electronics: [{ name: "Laptop", ... }, { name: "Phone", ... }], -- clothing: [{ name: "T-Shirt", ... }, { name: "Jeans", ... }] -- } -- Group into a Map (preserves key types, handles non-string keys) const byPriceRange = Map.groupBy(products, p => p.price < 100 ? "budget" : p.price < 500 ? "mid" : "premium" );
Common Interview Questions
Q: What is the difference between ?? and ||?
|| (logical OR) returns the right side when the left side is any falsy value: false, 0, "", null, undefined, NaN. ?? (nullish coalescing) only returns the right side when the left side is null or undefined. Use ?? when 0, "", and false are valid values you want to preserve.
Q: What is optional chaining and what does it return when the chain breaks?
Optional chaining (?.) short-circuits to undefined when a property or method access would be made on null or undefined, instead of throwing a TypeError. user?.address?.street returns undefined if user is null, or if user.address is null, or returns the street string if the full chain exists.
Q: What makes private class fields (using #) different from the convention of prefixing with underscore?
Underscore-prefixed properties (_balance) are just a convention β they are still fully accessible from outside the class. Private class fields using # are enforced by the JavaScript engine: accessing account.#balance from outside the class throws a SyntaxError at parse time, not just a convention violation. They are truly encapsulated.
Practice JavaScript on Froquiz
Modern JavaScript features are tested at every level of frontend and full-stack interviews. Test your JavaScript knowledge on Froquiz β from ES6 fundamentals to advanced patterns.
Summary
?.optional chaining returnsundefinedinstead of throwing on null access??nullish coalescing defaults only onnull/undefined, not all falsy values??=,||=,&&=logical assignment operators reduce conditional assignment boilerplateArray.at(-1)cleanly accesses the last element without.length - 1Object.hasOwn(obj, key)safely replaceshasOwnProperty.call()- Private class fields
#fieldare engine-enforced β truly inaccessible from outside structuredClone()deep-clones objects including Dates, Maps, Sets, and circular referencestoSorted(),toReversed(),with()are non-mutating array alternatives β ideal for React state