| |

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, splicemutate 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

MethodPurpose
.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

MethodPurpose
Object.hasOwn(o, k)Own property
Object.groupBy(arr, fn)Group into object
Object.fromEntries(entries)Pairs to object

String Methods

MethodPurpose
.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

MethodPurpose
Number.isFinite(x)Finite check
Number.isNaN(x)NaN check
Number.isInteger(x)Integer check
Number.EPSILONFloat tolerance
Number.MAX_SAFE_INTEGER2^53 – 1

RegExp

FeatureSyntax
Named groups(?<name>...)
Lookbehind(?<=...), (?<!...)
Indices/(x)/d
Match groupsmatch.groups.name
Match indicesmatch.indices[0]

Promise Methods

MethodPurpose
.all(arr)All or reject
.allSettled(arr)Always array
.race(arr)First to settle
.any(arr)First success

Errors

FeatureSyntax
Causenew Error('msg', { cause: err })
Accesserr.cause

Classes

FeatureSyntax
Private field#field = value
Private method#method() {}
Static fieldstatic field = value
Static blockstatic { ... }
Brand check#field in obj

Global

MethodPurpose
structuredClone(x)Deep copy
Array.fromAsync(iter)Async iterable to array
Top-level awaitawait 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

PitfallProblemSolution
.at() on undefinedTypeErrorGuard with ?.
toSorted without comparatorLexicographicPass comparator
replaceAll with regexMissing g flag throwsAdd g or use string
groupBy missing keysNull prototypeUse Map.groupBy
structuredClone on functionsThrowsOnly plain data
Error.cause not setundefinedAlways pass { cause }
Top-level await in CJSSyntaxErrorUse ESM
Old Node versionMethod missingCheck 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

FeatureVersionPurpose
.at()ES2022Negative indexing
Object.hasOwnES2022Safe own check
Error.causeES2022Error chaining
.findLast()ES2023Search from end
.toSorted()ES2023Immutable sort
.toReversed()ES2023Immutable reverse
.toSpliced()ES2023Immutable splice
.with()ES2023Immutable index
Object.groupByES2024Group into object
Map.groupByES2024Group into Map
Promise.allSettledES2020Wait for all
Promise.anyES2021First success
structuredCloneES2022Deep copy
Top-level awaitES2022Await in ESM
Array.fromAsyncES2025Async to array
Class fieldsES2022Public/private/static
Static blocksES2022Class init
Named regex groupsES2018(?<name>...)
Regex d flagES2022Match indices
replaceAllES2021Replace all

Key takeaways:

  • .at(-1) replaces arr[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 code
  • Object.hasOwn(obj, key) is the safe replacement for hasOwnProperty
  • Object.groupBy replaces the reduce loop for grouping
  • replaceAll works with strings and global regexes
  • padStart / padEnd for alignment and formatting
  • Number.isNaN and Number.isFinite don’t coerce — always use them over the globals
  • Promise.allSettled for partial failure; Promise.any for first success
  • Error.cause chains errors without losing the original
  • structuredClone is 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, static for class state, blocks for complex init
  • Named regex groups and the d flag make regex results more usable
  • Array.fromAsync reads 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!