JavaScript Modern and ES2024 Features

Practical modern JavaScript features you should be using today: cleaner array access, safe deep copies, better object utilities, and the latest ES2024 additions.

Intermediate 10 min read 10 examples

Array.at() and String.at()

The .at() method accepts positive and negative integers. Negative indices count from the end of the array or string. ES2022.

JavaScript
const fruits = ['apple', 'banana', 'cherry', 'date'];

// Positive index - same as bracket notation
console.log(fruits.at(0));  // 'apple'
console.log(fruits.at(1));  // 'banana'

// Negative index - counts from end
console.log(fruits.at(-1)); // 'date'   (last)
console.log(fruits.at(-2)); // 'cherry' (second to last)

// Old way - verbose
console.log(fruits[fruits.length - 1]); // 'date'

// Out of range - returns undefined
console.log(fruits.at(10));  // undefined
console.log(fruits.at(-10)); // undefined

// Works on strings too
const str = 'Hello';
console.log(str.at(0));  // 'H'
console.log(str.at(-1)); // 'o'

// Works on TypedArrays
const bytes = new Uint8Array([0, 127, 255]);
console.log(bytes.at(-1)); // 255

// Useful in function return
function getLastItem(arr) {
    return arr.at(-1); // much cleaner
}

// Use case: stack operations
const history = [];
history.push('/home', '/about', '/contact');
console.log(history.at(-1)); // '/contact' - current page

structuredClone()

The global structuredClone() creates a deep copy of an object using the structured clone algorithm. ES2022.

JavaScript
// Deep clone - nested objects are also copied
const original = {
    name: 'Alice',
    address: { city: 'Chennai', zip: '600001' },
    scores: [95, 87, 91],
};
const clone = structuredClone(original);

clone.address.city = 'Mumbai'; // does not affect original
console.log(original.address.city); // 'Chennai'

// Types that structuredClone handles correctly
const complex = {
    date:   new Date('2024-01-15'),    // preserved as Date object
    map:    new Map([['a', 1]]),       // preserved as Map
    set:    new Set([1, 2, 3]),        // preserved as Set
    regex:  /hello/gi,                 // preserved as RegExp
    buffer: new ArrayBuffer(8),        // preserved
    undef:  undefined,                 // preserved (JSON loses this)
};
const clone2 = structuredClone(complex);
console.log(clone2.date instanceof Date); // true (JSON gives a string)
console.log(clone2.map instanceof Map);   // true
console.log(clone2.undef);               // undefined (not lost)

// structuredClone DOES NOT handle:
// - Functions (throws DataCloneError)
// - DOM nodes (throws DataCloneError)
// - class instances (clones as plain object, loses methods)

// JSON.parse/stringify comparison
const bad = JSON.parse(JSON.stringify({
    date: new Date(),   // becomes a string
    undef: undefined,   // disappears entirely
    inf: Infinity,      // becomes null
    nan: NaN,           // becomes null
}));
console.log(bad.date instanceof Date); // false - it's a string now

Object.hasOwn()

A static method to safely check if an object has a property as its own (not inherited). ES2022 replacement for hasOwnProperty.

JavaScript
const user = { name: 'Alice', age: 30 };

// Object.hasOwn - preferred modern approach
console.log(Object.hasOwn(user, 'name')); // true
console.log(Object.hasOwn(user, 'age'));  // true
console.log(Object.hasOwn(user, 'toString')); // false (inherited from prototype)

// Old approach - hasOwnProperty (still works, just less safe)
console.log(user.hasOwnProperty('name')); // true

// Why hasOwn is safer - works even when hasOwnProperty is overridden or absent
const nullProto = Object.create(null); // no prototype, no hasOwnProperty!
nullProto.key = 'value';
// nullProto.hasOwnProperty('key'); // TypeError: not a function
Object.hasOwn(nullProto, 'key');    // true - always works

// Works when hasOwnProperty is shadowed
const tricky = { hasOwnProperty: () => false }; // overrides the method
// tricky.hasOwnProperty('x'); // returns false even if property exists!
tricky.x = 1;
Object.hasOwn(tricky, 'x'); // true - not fooled

// Iterating own properties safely
const obj = { a: 1, b: 2 };
for (const key in obj) {
    if (Object.hasOwn(obj, key)) {
        console.log(key, obj[key]); // only own properties
    }
}

Promise.any()

Resolves with the first fulfilled promise. Rejects with an AggregateError only if ALL promises reject. ES2021.

JavaScript
// Resolves with fastest fulfillment, ignores individual failures
const fastest = await Promise.any([
    fetch('https://cdn1.example.com/data.json'),
    fetch('https://cdn2.example.com/data.json'), // might be faster
    fetch('https://cdn3.example.com/data.json'),
]);
const data = await fastest.json();

// Only fails if ALL fail
try {
    await Promise.any([
        Promise.reject(new Error('CDN1 down')),
        Promise.reject(new Error('CDN2 down')),
        Promise.reject(new Error('CDN3 down')),
    ]);
} catch (err) {
    console.log(err instanceof AggregateError); // true
    console.log(err.errors.length);             // 3 - all individual errors
    err.errors.forEach(e => console.log(e.message));
}

// Comparison table:
// Promise.all        - ALL must fulfill; first rejection = reject
// Promise.allSettled - wait for ALL; never rejects; status per result
// Promise.race       - FIRST settled (fulfilled OR rejected) wins
// Promise.any        - FIRST fulfilled wins; only fails if ALL reject

// Use case: try multiple data sources, use whichever responds first
async function fetchFromAnySource(endpoints) {
    const results = await Promise.any(
        endpoints.map(url => fetch(url).then(r => r.json()))
    );
    return results;
}

Object.groupBy() and Map.groupBy()

Group array elements by a computed key. Returns a plain object (or Map). ES2024.

JavaScript
const products = [
    { name: 'Laptop',  category: 'Electronics', price: 999 },
    { name: 'Phone',   category: 'Electronics', price: 699 },
    { name: 'Shirt',   category: 'Clothing',    price: 49  },
    { name: 'Jeans',   category: 'Clothing',    price: 79  },
    { name: 'Monitor', category: 'Electronics', price: 399 },
];

// Object.groupBy - groups into a plain object
const byCategory = Object.groupBy(products, p => p.category);
console.log(byCategory);
// {
//   Electronics: [{ name: 'Laptop',...}, { name: 'Phone',...}, { name: 'Monitor',...}],
//   Clothing:    [{ name: 'Shirt',...}, { name: 'Jeans',...}]
// }
console.log(byCategory.Electronics.length); // 3

// Group by price range
const byPriceRange = Object.groupBy(products, p => {
    if (p.price < 100)  return 'budget';
    if (p.price < 500)  return 'mid-range';
    return 'premium';
});

// Map.groupBy - same but uses a Map (supports non-string keys)
const byPrice = Map.groupBy(products, p => Math.floor(p.price / 100) * 100);
console.log(byPrice.get(900)); // [{ name: 'Laptop',... }]

// Before Object.groupBy - using reduce
const manualGroup = products.reduce((acc, p) => {
    (acc[p.category] ??= []).push(p);
    return acc;
}, {});

Logical Assignment Operators

Combine logical operators with assignment. Only assigns if the condition holds. ES2021.

JavaScript
// ??= - assign only if left side is null or undefined
let config = null;
config ??= { theme: 'dark' };    // assigned - config was null
config ??= { theme: 'light' };   // NOT assigned - config already has a value
console.log(config.theme); // 'dark'

// Practical: initialize default values
function processUser(user) {
    user.name    ??= 'Anonymous';
    user.role    ??= 'viewer';
    user.prefs   ??= {};
    user.prefs.theme ??= 'system';
}

// ||= - assign only if left side is falsy (null, undefined, 0, '', false)
let count = 0;
count ||= 10; // assigned - 0 is falsy
console.log(count); // 10

let title = 'My Title';
title ||= 'Default'; // NOT assigned - 'My Title' is truthy
console.log(title); // 'My Title'

// &&= - assign only if left side is truthy
let user = { name: 'Alice' };
user &&= { ...user, active: true }; // assigned - user is truthy
console.log(user); // { name: 'Alice', active: true }

let guestUser = null;
guestUser &&= { ...guestUser, active: true }; // NOT assigned - null is falsy
console.log(guestUser); // null (unchanged)

// Comparison:
// a ??= b  is  a ?? (a = b)  - only null/undefined
// a ||= b  is  a || (a = b)  - any falsy value
// a &&= b  is  a && (a = b)  - truthy condition

Nullish Coalescing and Optional Chaining

Essential modern syntax for safely handling null and undefined. ES2020.

JavaScript
// ?? - nullish coalescing: use right side only for null/undefined
const port = process.env.PORT ?? 3000;
const name = user.name ?? 'Guest';    // '' stays '', null/undefined -> 'Guest'
const count = settings.count ?? 0;   // 0 stays 0, null/undefined -> 0

// vs || which treats ALL falsy as missing (0, '', false are lost)
const port2 = process.env.PORT || 3000; // 0 would become 3000 - bug!

// ?. - optional chaining: stop at null/undefined instead of throwing
const street = user?.address?.street; // undefined if any step is null/undefined
const first  = arr?.[0];              // undefined if arr is null/undefined
const result = obj?.method?.();       // undefined if method doesn't exist or obj is nullish

// Old way - verbose null checks
const old = user && user.address && user.address.street;

// Combining ?? and ?.
const city = user?.address?.city ?? 'Unknown';
const zip  = user?.address?.zip?.replace('-', '') ?? 'N/A';

// ?. in method calls
const len = str?.length;            // fine on any string or null/undefined
str?.trim()?.split(' ');            // chain method calls safely

// ?. with delete and assignment (returns undefined, doesn't throw)
delete user?.address?.street;       // no error even if user is null

// Practical: API response handling
async function getUser(id) {
    const res  = await fetch(`/api/users/${id}`);
    const data = await res.json();
    return {
        name:   data?.profile?.displayName ?? data?.username ?? 'Unknown',
        avatar: data?.profile?.avatar?.url ?? '/default-avatar.png',
        role:   data?.roles?.at(0) ?? 'member',
    };
}

More ES2024 Additions

JavaScript
// Promise.withResolvers() - ES2024
// Create a Promise and expose its resolve/reject externally
const { promise, resolve, reject } = Promise.withResolvers();
// Old way:
// let resolve, reject;
// const promise = new Promise((res, rej) => { resolve = res; reject = rej; });
setTimeout(() => resolve('done'), 1000);
const result = await promise; // 'done'

// Useful for deferred patterns
function createDeferred() {
    return Promise.withResolvers();
}
const deferred = createDeferred();
element.addEventListener('click', deferred.resolve, { once: true });
await deferred.promise; // wait for click

// Array.fromAsync() - ES2024 - like Array.from() but for async iterables
const rows = await Array.fromAsync(db.query('SELECT * FROM users'));
// vs old: const rows = []; for await (const row of cursor) rows.push(row);

// Well-formed Unicode strings - ES2024
const lone = '\uD800'; // lone surrogate (not valid Unicode)
console.log(lone.isWellFormed());      // false
console.log(lone.toWellFormed());      // replacement character
console.log('hello'.isWellFormed());   // true

// Temporal (proposal, not yet standard - but coming soon)
// const now = Temporal.Now.plainDateTimeISO();
// Much better than the Date object for complex date math

// Object.fromEntries - ES2019 (pairs well with modern features)
const entries = [['name', 'Alice'], ['age', 30]];
const obj = Object.fromEntries(entries); // { name: 'Alice', age: 30 }

// Transform object values via entries
const prices = { apple: 1.5, banana: 0.5, cherry: 2.0 };
const doubled = Object.fromEntries(
    Object.entries(prices).map(([k, v]) => [k, v * 2])
);

Frequently Asked Questions

Both access the last element, but array.at(-1) is more readable and concise. .at() accepts negative indices that count from the end: .at(-1) = last, .at(-2) = second-to-last. The old array[array.length - 1] requires computing the index manually. .at() also works on strings and TypedArrays. If the index is out of range, it returns undefined (same as bracket notation).

structuredClone() handles types that the JSON round-trip cannot: Date (preserved as Date, not a string), Map, Set, RegExp, undefined, Infinity, NaN, circular references (throws a useful error), and typed arrays. JSON silently loses or corrupts these types. Use structuredClone() for reliable deep copies.

Object.hasOwn(obj, key) works even when the object's prototype chain has been modified or when the object was created with Object.create(null) (which has no hasOwnProperty method). It also reads more clearly at the call site. It is the modern replacement for the awkward Object.prototype.hasOwnProperty.call(obj, key) pattern.

Promise.any() resolves with the first fulfilled promise and ignores rejections - it only rejects if ALL promises reject (with an AggregateError). Promise.race() settles with the first settled promise regardless of whether it fulfilled or rejected. So if the fastest promise rejects, race rejects too, but any keeps waiting for the next fulfillment.