JavaScript 70 🧬 Modern ES2022+ methods
JavaScript doesn’t stop moving. Every year, TC39 ships new features — some big, some small, all designed to remove boilerplate and fix sharp edges. Between ES2022 and the latest proposals, a set of methods and syntax has landed that most developers haven’t caught up with yet.
This chapter covers what’s actually in the language now, or nearly — the methods that solve real problems and are already supported in Node 18+, modern Chrome, and Safari. If you write JavaScript in 2025, these belong in your toolkit.
Key point: These aren’t proposals or experiments. They’re ratified features with wide support. The only reason to avoid them is if you target very old browsers.
a – Array and Object additions
Arrays and objects got the most new methods. Several of them fix long-standing annoyances.
Array.prototype.at() — negative indexing:
The old way to get the last element was arr[arr.length - 1]. Now you can write:
const arr = [1, 2, 3, 4, 5];
arr.at(-1); // 5
arr.at(-2); // 4
arr.at(0); // 1
.at() accepts negative indices — -1 is the last element, -2 is the one before, and so on. It works on strings too.
Array.prototype.findLast() and findLastIndex():
.find() returns the first match. These return the last:
const nums = [1, 2, 3, 4, 3, 2, 1];
nums.findLast(n => n > 2); // 3
nums.findLastIndex(n => n > 2); // 4
Useful for searching from the end without reversing or iterating backwards.
Array.prototype.toSorted(), toReversed(), toSpliced(), with():
The original methods — sort, reverse, splice — mutate the array. These do the same thing but return a new array:
const arr = [3, 1, 2];
arr.toSorted(); // [1, 2, 3]
arr.toReversed(); // [2, 1, 3]
arr.toSpliced(1, 1, 'x'); // [3, 'x', 2]
arr.with(1, 99); // [3, 99, 2]
arr is unchanged. This is a huge win for immutable code — no more [...arr].sort().
Array.prototype.flatMap():
Already widely used, but worth mentioning. It maps and flattens one level:
const words = ['hello', 'world'];
words.flatMap(w => w.split('')); // ['h','e','l','l','o','w','o','r','l','d']
Object.hasOwn() — safer hasOwnProperty:
The old way had edge cases: obj.hasOwnProperty could be shadowed, and using it on a null-prototype object failed.
const obj = { a: 1 };
Object.hasOwn(obj, 'a'); // true
Object.hasOwn(obj, 'b'); // false
It’s a static method, so it works on any object — including those created with Object.create(null).
Object.groupBy() and Map.groupBy() — grouping:
New in ES2024. Group an array by a key function:
const people = [
{ name: 'Alice', dept: 'Eng' },
{ name: 'Bob', dept: 'Sales' },
{ name: 'Carol', dept: 'Eng' }
];
const byDept = Object.groupBy(people, p => p.dept);
// { Eng: [{...}, {...}], Sales: [{...}] }
Before this, you had to write a reduce loop every time. Now it’s one call. Use Map.groupBy when keys aren’t strings.
Array.fromAsync() — async iterables to arrays:
New in ES2025. Reads an async iterable into an array:
async function* gen() {
yield 1;
yield 2;
yield 3;
}
const arr = await Array.fromAsync(gen());
// [1, 2, 3]
Perfect for reading paginated APIs or async generators.
b – String, Number, and RegExp additions
Strings and numbers got targeted improvements.
String.prototype.replaceAll() — replace every occurrence:
The old replace only replaced the first match unless you used a global regex. replaceAll replaces every occurrence:
'hello world'.replaceAll('o', '0');
// 'hell0 w0rld'
Works with strings and global regexes. If you pass a regex without the g flag, it throws.
String.prototype.at() — negative indexing for strings:
'hello'.at(-1); // 'o'
'hello'.at(0); // 'h'
Same idea as the array method.
String.prototype.trimStart() and trimEnd():
Instead of trim() which removes both ends, these remove one side:
' hello '.trimStart(); // 'hello '
' hello '.trimEnd(); // ' hello'
String.prototype.padStart() and padEnd():
Pad a string to a target length:
'5'.padStart(3, '0'); // '005'
'5'.padEnd(3, '0'); // '500'
Useful for formatting numbers, timestamps, and IDs.
Number.isFinite, Number.isNaN, Number.isInteger:
These static methods check without coercing — unlike the global isFinite, isNaN:
Number.isFinite('42'); // false
Number.isNaN('abc'); // false
isFinite('42'); // true ← coerces
isNaN('abc'); // true ← coerces
Use the Number.* versions. The globals lie.
Number.parseInt and Number.parseFloat:
The same as the globals but namespaced under Number. Not new, but part of the same modernization.
Number.EPSILON, MAX_SAFE_INTEGER, MIN_SAFE_INTEGER:
Constants for numeric safety:
Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON; // true
Number.MAX_SAFE_INTEGER; // 9007199254740991
RegExp — d flag (indices):
Adds start and end indices for matches and capture groups:
const re = /(\d+)/d;
const match = re.exec('abc123');
match.indices[0]; // [3, 6]
match.indices[1]; // [3, 6]
Useful for editors and text processing — you know exactly where each match is.
RegExp — lookbehind ((?<=...), (?<!...)):
Already covered in the regex chapter, but part of the same modernization era. Lookbehind asserts what came before:
'$100'.match(/(?<=\$)\d+/)[0]; // '100'
RegExp — named capture groups ((?<name>...)):
Instead of numbered groups, name them:
const re = /(?<year>\d{4})-(?<month>\d{2})/;
const m = '2024-01'.match(re);
m.groups.year; // '2024'
m.groups.month; // '01'
Much more readable than m[1], m[2].
c – Async, error handling, and class features
The biggest modern additions are in async code and class syntax.
Promise.allSettled() — wait for all, never reject:
Promise.all rejects as soon as any Promise rejects. allSettled waits for all and returns their outcomes:
const results = await Promise.allSettled([
fetch('/a'),
fetch('/b'),
fetch('/c')
]);
results.forEach(r => {
if (r.status === 'fulfilled') console.log(r.value);
else console.log('Failed:', r.reason);
});
Ideal for batch operations where partial failure is acceptable.
Promise.any() — first success:
Promise.race settles with the first Promise, win or lose. Promise.any resolves with the first success, ignoring rejections until all fail:
const fastest = await Promise.any([
fetch('/primary'),
fetch('/backup')
]);
Useful for redundant sources — get whichever responds successfully first.
Error.cause — chained errors:
Wrap a low-level error inside a high-level one without losing the original:
try {
JSON.parse('bad');
} catch (err) {
throw new Error('Config failed', { cause: err });
}
The original error is preserved on err.cause. Invaluable for debugging nested failures.
Class fields — public/private/static:
Already covered in chapter 60. Public fields remove constructor boilerplate; # makes state truly private; static puts members on the class.
Private methods and accessors:
class Counter {
#count = 0;
#increment() { this.#count++; }
tick() { this.#increment(); }
}
# works on methods and accessors too — not just fields.
Static blocks:
Run code once, at class definition time:
class Config {
static #cache;
static {
Config.#cache = new Map();
}
}
For complex static initialization that needs multiple statements.
Object.hasOwn (again, but here for classes):
Checking if a class instance has an own property:
Object.hasOwn(instance, 'name');
Never fails on null-prototype objects.
Array.prototype.at in class methods:
Combined with the array additions, class code gets shorter:
class Queue {
#items = [];
peek() { return this.#items.at(-1); }
next() { return this.#items.shift(); }
}
Error.cause in async code:
async function loadConfig() {
try {
return await readFile('config.json');
} catch (err) {
throw new Error('Config load failed', { cause: err });
}
}
The stack trace shows both errors — the wrapper and the original.
Top-level await:
In ES modules, you can await at the top level — no wrapper function needed:
const config = await fetch('/config').then(r => r.json());
export default config;
Works in .mjs files, "type": "module", and modern bundlers.
structuredClone() — deep copy:
Before this, deep cloning required JSON.parse(JSON.stringify(x)) — which lost functions, dates, Maps, and Sets. structuredClone handles them all:
const obj = { a: 1, b: { c: 2 }, d: new Date() };
const clone = structuredClone(obj);
Works with plain objects, arrays, Dates, Maps, Sets, RegExps, Blobs, ArrayBuffers. Doesn’t clone functions or DOM nodes.
WeakRef and FinalizationRegistry:
Covered in chapter 59 — advanced memory tools for caches and native cleanup.
Symbol.hasInstance, Symbol.toPrimitive:
Metaprogramming hooks covered in chapter 52.
The at, findLast, toSorted family:
All the small array additions add up. Modern code uses .at(-1), .findLast(), and .toSorted() where older code had clunky workarounds.
Optional chaining and nullish coalescing:
Though technically ES2020, they’re worth restating because they pair with everything else:
const city = user?.address?.city ?? 'Unknown';
They’ve become so common they feel foundational.
Complete Example Session
// ============================================
// PART 1: .at()
// ============================================
const arr = [1, 2, 3, 4, 5];
console.log(arr.at(-1));
// [ 5 ]
console.log('hello'.at(-1));
// [ 'o' ]
// ============================================
// PART 2: findLast / findLastIndex
// ============================================
const nums = [1, 2, 3, 4, 3, 2, 1];
console.log(nums.findLast(n => n > 2));
// [ 3 ]
console.log(nums.findLastIndex(n => n > 2));
// [ 4 ]
// ============================================
// PART 3: toSorted / toReversed
// ============================================
const original = [3, 1, 2];
const sorted = original.toSorted();
console.log(sorted);
// [ [ 1, 2, 3 ] ]
console.log(original);
// [ [ 3, 1, 2 ] ] ← unchanged
// ============================================
// PART 4: with()
// ============================================
const arr2 = [1, 2, 3];
console.log(arr2.with(1, 99));
// [ [ 1, 99, 3 ] ]
// ============================================
// PART 5: Object.hasOwn
// ============================================
const obj = { a: 1 };
console.log(Object.hasOwn(obj, 'a'));
// [ true ]
console.log(Object.hasOwn(obj, 'b'));
// [ false ]
// ============================================
// PART 6: Object.groupBy
// ============================================
const items = [
{ name: 'apple', type: 'fruit' },
{ name: 'carrot', type: 'veg' },
{ name: 'banana', type: 'fruit' }
];
const grouped = Object.groupBy(items, i => i.type);
console.log(Object.keys(grouped));
// [ [ 'fruit', 'veg' ] ]
// ============================================
// PART 7: replaceAll
// ============================================
console.log('hello world'.replaceAll('o', '0'));
// [ 'hell0 w0rld' ]
// ============================================
// PART 8: padStart
// ============================================
console.log('5'.padStart(3, '0'));
// [ '005' ]
// ============================================
// PART 9: Array.fromAsync
// ============================================
async function* gen() {
yield 1;
yield 2;
}
const result = await Array.fromAsync(gen());
console.log(result);
// [ [ 1, 2 ] ]
// ============================================
// PART 10: Promise.allSettled
// ============================================
const settled = await Promise.allSettled([
Promise.resolve(1),
Promise.reject(new Error('x'))
]);
console.log(settled.map(r => r.status));
// [ [ 'fulfilled', 'rejected' ] ]
// ============================================
// PART 11: Promise.any
// ============================================
const first = await Promise.any([
Promise.reject(new Error('a')),
Promise.resolve('b')
]);
console.log(first);
// [ 'b' ]
// ============================================
// PART 12: Error.cause
// ============================================
try {
throw new Error('Wrapper', { cause: new Error('Original') });
} catch (err) {
console.log(err.message);
// [ 'Wrapper' ]
console.log(err.cause.message);
// [ 'Original' ]
}
// ============================================
// PART 13: structuredClone
// ============================================
const data = { a: 1, b: { c: 2 } };
const clone = structuredClone(data);
clone.b.c = 99;
console.log(data.b.c);
// [ 2 ] ← unchanged
// ============================================
// PART 14: Top-level await
// ============================================
// In a .mjs file or type: module
// const config = await fetch('/config').then(r => r.json());
// ============================================
// PART 15: Named capture groups
// ============================================
const m = '2024-01-15'.match(/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/);
console.log(m.groups.y);
// [ '2024' ]
// ============================================
// PART 16: RegExp d flag
// ============================================
const re = /(\d+)/d;
const match = re.exec('abc123');
console.log(match.indices[0]);
// [ [ 3, 6 ] ]
Quick Reference
Array Methods
| Method | Purpose |
|---|---|
.at(-1) | Last element |
.findLast(fn) | Last match |
.findLastIndex(fn) | Last match index |
.toSorted() | Sorted copy |
.toReversed() | Reversed copy |
.toSpliced() | Spliced copy |
.with(i, v) | Replace at index |
.flatMap(fn) | Map + flatten |
.groupBy(fn) (static) | Group by key |
Object Methods
| Method | Purpose |
|---|---|
Object.hasOwn(o, k) | Own property |
Object.groupBy(arr, fn) | Group into object |
Object.fromEntries(entries) | Pairs to object |
String Methods
| Method | Purpose |
|---|---|
.at(-1) | Last character |
.replaceAll(a, b) | Replace all |
.trimStart() | Trim left |
.trimEnd() | Trim right |
.padStart(n, c) | Pad left |
.padEnd(n, c) | Pad right |
Number Methods
| Method | Purpose |
|---|---|
Number.isFinite(x) | Finite check |
Number.isNaN(x) | NaN check |
Number.isInteger(x) | Integer check |
Number.EPSILON | Float tolerance |
Number.MAX_SAFE_INTEGER | 2^53 – 1 |
RegExp
| Feature | Syntax |
|---|---|
| Named groups | (?<name>...) |
| Lookbehind | (?<=...), (?<!...) |
| Indices | /(x)/d |
| Match groups | match.groups.name |
| Match indices | match.indices[0] |
Promise Methods
| Method | Purpose |
|---|---|
.all(arr) | All or reject |
.allSettled(arr) | Always array |
.race(arr) | First to settle |
.any(arr) | First success |
Errors
| Feature | Syntax |
|---|---|
| Cause | new Error('msg', { cause: err }) |
| Access | err.cause |
Classes
| Feature | Syntax |
|---|---|
| Private field | #field = value |
| Private method | #method() {} |
| Static field | static field = value |
| Static block | static { ... } |
| Brand check | #field in obj |
Global
| Method | Purpose |
|---|---|
structuredClone(x) | Deep copy |
Array.fromAsync(iter) | Async iterable to array |
| Top-level await | await in ESM |
Best Practices
✅ Do This:
// Use .at(-1) for the last element
const last = arr.at(-1); // ✅
// Use toSorted for immutable sort
const sorted = arr.toSorted((a, b) => a - b); // ✅
// Use Object.hasOwn
Object.hasOwn(obj, 'key'); // ✅
// Use groupBy instead of reduce
const grouped = Object.groupBy(items, i => i.type); // ✅
// Use replaceAll
'abc'.replaceAll('a', 'x'); // ✅
// Use Promise.allSettled for partial failure
const results = await Promise.allSettled(tasks); // ✅
// Use Error.cause for chained errors
throw new Error('Outer', { cause: err }); // ✅
// Use structuredClone
const copy = structuredClone(original); // ✅
// Use named regex groups
/(?<year>\d{4})/.exec(str).groups.year; // ✅
❌ Don’t Do This:
// Don't use length-1 for last
arr[arr.length - 1]; // ⚠️ use .at(-1)
// Don't mutate when you don't need to
arr.sort(); // ⚠️ use toSorted()
// Don't rely on hasOwnProperty
obj.hasOwnProperty('x'); // ⚠️ use Object.hasOwn
// Don't JSON-clone for deep copy
JSON.parse(JSON.stringify(obj)); // ⚠️ loses types
// Don't use global isNaN
isNaN('abc'); // ❌ coerces
Number.isNaN('abc'); // ✅
// Don't chain .catch everywhere for batch
tasks.map(t => t.catch(handle)); // ⚠️ use allSettled
// Don't lose error context
catch (e) { throw new Error('failed'); } // ⚠️ use cause
// Don't parse regex by index
match[1]; // ⚠️ use named groups
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
.at() on undefined | TypeError | Guard with ?. |
toSorted without comparator | Lexicographic | Pass comparator |
replaceAll with regex | Missing g flag throws | Add g or use string |
groupBy missing keys | Null prototype | Use Map.groupBy |
structuredClone on functions | Throws | Only plain data |
Error.cause not set | undefined | Always pass { cause } |
| Top-level await in CJS | SyntaxError | Use ESM |
| Old Node version | Method missing | Check engines |
Real-World Examples
1. Last element
const items = ['a', 'b', 'c'];
console.log(items.at(-1));
// [ 'c' ]
Cleaner than items[items.length - 1].
2. Last matching item
const logs = [{ level: 'info' }, { level: 'error' }, { level: 'info' }];
const lastError = logs.findLast(l => l.level === 'error');
Search backwards without reversing.
3. Immutable sort
const sorted = [...users].sort((a, b) => a.age - b.age);
// becomes
const sorted = users.toSorted((a, b) => a.age - b.age);
No more spread-before-sort.
4. Update an element immutably
const next = state.with(index, newValue);
Cleaner than spreading and replacing.
5. Safe property check
if (Object.hasOwn(config, 'port')) { ... }
Works on any object — null-prototype included.
6. Group by property
const byCategory = Object.groupBy(products, p => p.category);
One call replaces a reduce.
7. Replace all occurrences
const cleaned = input.replaceAll(/[^a-z0-9]/gi, '');
Global regex — every match.
8. Wait for all async results
const results = await Promise.allSettled(urls.map(u => fetch(u)));
const succeeded = results.filter(r => r.status === 'fulfilled');
No batch failure from one rejection.
9. First successful response
const data = await Promise.any([fetch('/a'), fetch('/b')]);
Whichever succeeds first wins.
10. Chain errors
try { parseConfig(); }
catch (err) { throw new Error('Init failed', { cause: err }); }
Both errors preserved for debugging.
Visual: Mutating vs Non-Mutating
┌──────────────────────────────────────────────┐
│ Mutating (old) │
│ │
│ arr.sort() ← changes arr │
│ arr.reverse() ← changes arr │
│ arr.splice() ← changes arr │
│ │
│ Need a copy? [...arr].sort() │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Non-mutating (new) │
│ │
│ arr.toSorted() ← new array │
│ arr.toReversed() ← new array │
│ arr.toSpliced() ← new array │
│ arr.with(i, v) ← new array │
│ │
│ Original unchanged │
│ │
└──────────────────────────────────────────────┘
Visual: Promise Combinators
┌──────────────────────────────────────────────┐
│ all │
│ │
│ ✓ ✓ ✓ → resolves │
│ ✓ ✗ ✓ → rejects immediately │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ allSettled │
│ │
│ ✓ ✓ ✓ → [{ok}, {ok}, {ok}] │
│ ✓ ✗ ✓ → [{ok}, {err}, {ok}] │
│ │
│ Never rejects │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ race │
│ │
│ First to settle wins (success or fail) │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ any │
│ │
│ First SUCCESS wins; rejects only if all do │
│ │
└──────────────────────────────────────────────┘
Visual: Error Chaining
┌──────────────────────────────────────────────┐
│ High-level error │
│ │
│ Error: Config failed │
│ message: 'Config failed' │
│ cause: ─────────────────┐ │
│ │ │
│ ▼ │
│ Low-level error │ │
│ │ │
│ Error: ENOENT │ │
│ message: 'file not found' │
│ │
│ Both preserved in the stack trace │
│ │
└──────────────────────────────────────────────┘
Summary
| Feature | Version | Purpose |
|---|---|---|
.at() | ES2022 | Negative indexing |
Object.hasOwn | ES2022 | Safe own check |
Error.cause | ES2022 | Error chaining |
.findLast() | ES2023 | Search from end |
.toSorted() | ES2023 | Immutable sort |
.toReversed() | ES2023 | Immutable reverse |
.toSpliced() | ES2023 | Immutable splice |
.with() | ES2023 | Immutable index |
Object.groupBy | ES2024 | Group into object |
Map.groupBy | ES2024 | Group into Map |
Promise.allSettled | ES2020 | Wait for all |
Promise.any | ES2021 | First success |
structuredClone | ES2022 | Deep copy |
| Top-level await | ES2022 | Await in ESM |
Array.fromAsync | ES2025 | Async to array |
| Class fields | ES2022 | Public/private/static |
| Static blocks | ES2022 | Class init |
| Named regex groups | ES2018 | (?<name>...) |
Regex d flag | ES2022 | Match indices |
replaceAll | ES2021 | Replace all |
Key takeaways:
.at(-1)replacesarr[arr.length - 1]— works on arrays and strings.findLast()and.findLastIndex()search from the end.toSorted(),.toReversed(),.toSpliced(),.with()are non-mutating versions — huge for immutable codeObject.hasOwn(obj, key)is the safe replacement forhasOwnPropertyObject.groupByreplaces the reduce loop for groupingreplaceAllworks with strings and global regexespadStart/padEndfor alignment and formattingNumber.isNaNandNumber.isFinitedon’t coerce — always use them over the globalsPromise.allSettledfor partial failure;Promise.anyfor first successError.causechains errors without losing the originalstructuredCloneis the correct way to deep copy — handles Dates, Maps, Sets- Top-level await works in ES modules — no wrapper needed
- Class fields with
#for real privacy,staticfor class state, blocks for complex init - Named regex groups and the
dflag make regex results more usable Array.fromAsyncreads async iterables into arrays
Remember: JavaScript moves forward every year. The features in this chapter aren’t proposals — they’re in every modern runtime. Adopt them: .at(-1) for last elements, .toSorted() for immutable operations, Object.hasOwn for safe checks, Object.groupBy for grouping, structuredClone for deep copies, Error.cause for chained errors. Use Promise.allSettled when you need every result, Promise.any when you need the first success. These features remove boilerplate, fix sharp edges, and make modern JavaScript cleaner. Master them, and your code reads like the language it’s written in — current, not historical.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!