FroquizFroquiz
HomeQuizzesSenior ChallengeGet CertifiedBlogAbout
Sign InStart Quiz
Sign InStart Quiz
Froquiz

The most comprehensive quiz platform for software engineers. Test yourself with 10000+ questions and advance your career.

LinkedIn

Platform

  • Start Quizzes
  • Topics
  • Blog
  • My Profile
  • Sign In

About

  • About Us
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

Β© 2026 Froquiz. All rights reserved.Built with passion for technology
Blog & Articles

Modern JavaScript: ES2020 to ES2024 Features You Should Be Using

Catch up on the most useful modern JavaScript features. Covers optional chaining, nullish coalescing, logical assignment, Array.at(), Object.hasOwn, Promise.any, structuredClone, and more.

Yusuf SeyitoğluMarch 17, 20262 views9 min read

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:

javascript
const 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):

javascript
const 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:

javascript
const 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:

javascript
async 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

javascript
class 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

javascript
const 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:

javascript
const 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:

javascript
const 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 returns undefined instead of throwing on null access
  • ?? nullish coalescing defaults only on null/undefined, not all falsy values
  • ??=, ||=, &&= logical assignment operators reduce conditional assignment boilerplate
  • Array.at(-1) cleanly accesses the last element without .length - 1
  • Object.hasOwn(obj, key) safely replaces hasOwnProperty.call()
  • Private class fields #field are engine-enforced β€” truly inaccessible from outside
  • structuredClone() deep-clones objects including Dates, Maps, Sets, and circular references
  • toSorted(), toReversed(), with() are non-mutating array alternatives β€” ideal for React state

About Author

Yusuf Seyitoğlu

Author β†’

Other Posts

  • Python Testing with pytest: Fixtures, Parametrize, Mocking and Best PracticesMar 17
  • PostgreSQL Full-Text Search: tsvector, tsquery, Ranking and Multilingual SearchMar 17
  • Software Architecture Patterns: MVC, Clean Architecture, Hexagonal and Event-DrivenMar 17
All Blogs