Modern ECMAScript feature reference

A support checklist for confirming which modern JavaScript features Cruft implements, weighted toward recent additions like iterator helpers, explicit resource management, and change-by-copy array methods, with runnable examples. Notes the one caveat: WeakRef and FinalizationRegistry never collect.

Cruft implements the ECMAScript language spec directly. There is no transpilation step down to an older baseline. The whole modern grammar and standard library are present: the engine passes the full test262 conformance suite with zero skipped tests, so the overwhelming majority of the language simply works, including the parts an independent engine is most likely to get wrong.

This page is a reference for confirming feature support, weighted toward the recent and edge additions. It is not a tutorial; it assumes you know JavaScript. For how a program actually executes (parse to bytecode to Exegesis to LeJIT), see the execution model; for how values, objects, and intrinsics are represented, see Cruft Core. This page does not repeat either.

Feature status at a glance

Feature (edition)StatusNotes
Private fields / methods #xinstance + static
Static initialization blocks
Ergonomic brand check #x in obj
Class accessors (get/set)
Iterator helpers map/filter/take/drop/flatMap/reduce/toArray (ES2025)
Iterator.from
using / await using (explicit resource mgmt)LIFO disposal
Symbol.dispose / Symbol.asyncDispose
DisposableStackdefer, dispose
Object.groupBy / Map.groupBy
Array.prototype.groupabsent (by design)never standardized; use Object.groupBy
Array.prototype.findLast / findLastIndex
Array.prototype.at / String.prototype.at
toSorted / toReversed / toSpliced / with (ES2023)non-mutating
Object.hasOwn
String.prototype.replaceAll
String.prototype.isWellFormed / toWellFormed (ES2024)
Promise.withResolvers (ES2024)
Promise.any / AggregateError
Array.fromAsync (ES2024)
Error cause new Error(msg, {cause})
RegExp v flag (unicodeSets, ES2024)see regexp
BigInt
Float16Array (ES2025) / Uint8ClampedArray
Resizable ArrayBuffer (maxByteLength + resize)
ArrayBuffer.prototype.transfersource detaches
WeakRef / FinalizationRegistrypartial (silent)present but hold strongly, never collect, see below
Well-known symbolsincl. dispose, asyncDispose, matchAll
Top-level await (modules)
import.meta / dynamic import()
Labeled statements
Optional catch binding
Numeric separators 1_000
Logical assignment operatorsnullish, and, and or-assignment

Classes

Private state, static blocks, and the ergonomic brand check all behave to spec. The brand check #x in obj returns true only for instances that carry the private field, without throwing on foreign objects.

class A {
  #x = 1;
  static blk;
  static { this.blk = 42; }
  #m() { return this.#x; }
  get v() { return this.#x; }
  has(o) { return #x in o; }
  run() { return this.#m(); }
}
const a = new A();
console.log(a.v, a.run(), a.has(a), a.has({}), A.blk);
// 1 1 true false 42

Iterator helpers (ES2025)

The full lazy iterator-helper protocol is present on Iterator.prototype, plus the Iterator.from adapter.

const it = [1, 2, 3, 4, 5].values();
console.log([...it.map(x => x * 2).filter(x => x > 4).take(2)]); // [ 6, 8 ]
console.log(Iterator.from([9, 8]).toArray());                    // [ 9, 8 ]
console.log([1, 2, 3].values().reduce((a, b) => a + b, 0));      // 6
console.log([1, 2].values().flatMap(x => [x, x]).toArray());     // [ 1, 1, 2, 2 ]

Explicit resource management

using and await using declarations, the Symbol.dispose / Symbol.asyncDispose protocol, and DisposableStack are implemented. Disposal runs in reverse (LIFO) order at block exit.

function mk(n) { return { [Symbol.dispose]() { console.log("disp", n); } }; }
{ using a = mk("a"); using b = mk("b"); console.log("body"); }
// body
// disp b
// disp a

await using awaits the async disposer before leaving the block:

async function main() {
  const r = { async [Symbol.asyncDispose]() { console.log("adisp"); } };
  { await using x = r; console.log("abody"); }
}
main(); // abody \n adisp

Arrays and objects

Grouping, non-mutating array methods (change-array-by-copy), and the recent lookup helpers are all present. Note that Array.prototype.group was never standardized (it lost to Object.groupBy) and is correctly absent.

console.log(JSON.stringify(Object.groupBy([1, 2, 3, 4], x => x % 2 ? "o" : "e")));
// {"o":[1,3],"e":[2,4]}
console.log([3, 1, 2].toSorted(), [1, 2, 3].with(1, 9), [1, 2, 3].toSpliced(1, 1));
// [ 1, 2, 3 ] [ 1, 9, 3 ] [ 1, 3 ]
console.log([1, 2, 3].at(-1), [1, 2, 3].findLast(x => x < 3), Object.hasOwn({ a: 1 }, "a"));
// 3 2 true

Strings

console.log("abc".at(-1), "a.b.c".replaceAll(".", "-")); // c a-b-c
const bad = "\uD800"; // lone surrogate
console.log(bad.isWellFormed(), bad.toWellFormed().length, "ok".isWellFormed());
// false 1 true

Promises and async

Promise.withResolvers, Promise.any with AggregateError, and Array.fromAsync are all present and correct.

const { promise, resolve } = Promise.withResolvers();
resolve(7); promise.then(v => console.log("wr", v)); // wr 7

Promise.any([Promise.reject(1)]).catch(e =>
  console.log(e instanceof AggregateError, e.errors)); // true [ 1 ]

Array.fromAsync([Promise.resolve(1), Promise.resolve(2)])
  .then(a => console.log(a)); // [ 1, 2 ]

Errors, RegExp, BigInt, binary data

console.log(new Error("boom", { cause: "root" }).cause); // root

const re = /[\p{ASCII}]/v;
console.log(re.flags, re.unicodeSets); // v true

console.log(10n ** 3n, new Float16Array([1.5])[0]); // 1000n 1.5

const ab = new ArrayBuffer(8, { maxByteLength: 16 });
console.log(ab.resizable, ab.maxByteLength); ab.resize(12);
console.log(ab.byteLength); // true 16 \n 12

const src = new ArrayBuffer(4);
const moved = src.transfer();
console.log(src.detached, moved.byteLength); // true 4

The RegExp v flag (Unicode sets) is present; its full semantics are covered in the RegExp reference.

Modules

Top-level await, import.meta, and dynamic import() work in an ES module (.mjs, or a .js under a "type": "module" package). See the module system for resolution details.

// m.mjs
console.log(import.meta.url.endsWith("m.mjs")); // true
const v = await Promise.resolve(99);            // top-level await
const mod = await import("./dep.mjs");          // dynamic import
console.log(v, mod.hello);                       // 99 hi

Structured control and operators

Labeled break/continue, optional catch binding, numeric separators, and logical assignment are all in place.

outer: for (let i = 0; i < 3; i++)
  for (let j = 0; j < 3; j++) { if (j === 1) continue outer; console.log(i, j); }
try { throw 1; } catch { console.log("no binding"); }
let a = null; a ??= 5; let b = 1; b &&= 2; let c = 0; c ||= 3;
console.log(1_000_000, a, b, c); // 1000000 5 2 3

The one caveat: weak references

WeakRef and FinalizationRegistry are the single feature on this page that does not behave to spec, and the divergence is silent rather than an error. Both constructors exist and their methods (deref, register) return sensible values, so code that uses them runs without complaint. But the referents are held strongly: a WeakRef never observes its target being collected, and a FinalizationRegistry callback never fires.

const wr = new WeakRef({ a: 1 });
console.log(wr.deref().a); // 1 — and stays reachable forever

The practical consequence: a weakly-keyed cache built on these primitives becomes unbounded strong retention with no signal (after churning allocations under GC pressure, a WeakRef to an otherwise-unreachable object still derefs to it, and no finalizer runs). The weak semantics are not yet wired: the intrinsics are strong-holding placeholders, pending GC weak-slot support and post-sweep finalization jobs. Treat WeakRef and FinalizationRegistry as functionally present but non-collecting until that lands. Every other feature on this page holds to spec.