Nub fills in modern JavaScript, web-platform, and Node APIs that your Node version lacks. Each entry gives the Node versions where the API is available and how Nub delivers it.

TypeScript setup

Nub's types package declares the proposal APIs that TypeScript's libraries do not include yet, plus Temporal, the browser-style Worker global, reportError, and the node:util rate helpers. Other standard APIs use TypeScript's bundled libraries and @types/node.

# install the package
npm i -D @nubjs/types @types/node@25

# tsconfig.json
{ "compilerOptions": { "types": ["node", "@nubjs/types"] } }

JavaScript APIs

These are ECMAScript globals and methods. Each entry gives the Node version that ships it and how Nub supplies it below that version.

Temporal

The Temporal namespace provides immutable types for instants, calendar dates, durations, and time zones. Native on Node 26+; on older Node, Nub loads @js-temporal/polyfill the first time code reads Temporal.

const deployed = Temporal.Instant.from("2026-08-07T16:30:00Z");
const local = deployed.toZonedDateTimeISO("America/Los_Angeles");
console.log(local.add({ days: 1 }).toString());

// an existing Date converts too
const fromDate = new Date("2026-08-07T16:30:00Z").toTemporalInstant();
console.log(fromDate.equals(deployed)); // true

Promise.allKeyed()

The keyed combinators take an object of promises and resolve to an object of results. No Node release ships the await-dictionary proposal yet; Nub polyfills it.

// rejects on the first rejection, like Promise.all
const { account, invoices } = await Promise.allKeyed({
  account: getAccount(accountId),
  invoices: listInvoices(accountId),
});

// records every outcome, like Promise.allSettled
const outcomes = await Promise.allSettledKeyed({
  profile: getProfile(account.id),
  avatar: getAvatar(account.id),
});

Promise.withResolvers()

The Promise.withResolvers() method creates a promise together with its resolve and reject functions. Native on Node 22+; Nub polyfills it below.

const { promise, resolve } = Promise.withResolvers<string>();
queueMicrotask(() => resolve("ready"));
console.log(await promise); // "ready"

Promise.try()

The Promise.try() method calls a function and turns its return value or thrown error into a promise. Native on Node 23+; Nub polyfills it below.

// a synchronous throw becomes a rejection
const config = await Promise.try(() => JSON.parse(source));

Iterator helpers

The iterator helpers transform and consume iterators lazily, without building an array first. Native on Node 22+; Nub polyfills them below.

const activeNames = Iterator.from(users)
  .filter((user) => user.active)
  .map((user) => user.name)
  .take(10)
  .toArray();

The supplied helpers are:

map  filter  take  drop  flatMap  reduce  toArray
some  every  find  forEach

Iterator.concat()

The Iterator.concat() method visits several iterables in sequence, lazily. Native on Node 26+; Nub polyfills it below.

const allIds = Iterator.concat(primaryIds, archivedIds).toArray();

Iterator.zip()

The joint-iteration methods advance several iterables in step. No Node release ships them yet; Nub polyfills them.

// yields [id, name] pairs
const pairs = Iterator.zip([ids, names]).toArray();

// yields { id, name } rows
for (const row of Iterator.zipKeyed({ id: ids, name: names })) {
  console.log(row.id, row.name);
}

Iterator.prototype.chunks()

These helpers batch or window an iterator, search it for a value, or join it into a string. No Node release ships them yet; Nub polyfills them.

const batches = Iterator.from(records).chunks(100).toArray();

// overlapping windows: [1, 2, 3], [2, 3, 4], ...
const triples = Iterator.from(values).windows(3).toArray();

// [[1, 2]], one short window
Iterator.from([1, 2]).windows(3, "allow-partial").toArray();

const hasDraft = Iterator.from(statuses).includes("draft");
const label = Iterator.from(tags).join(", ");

Set.prototype.union()

The Set methods combine and compare sets. Native on Node 22+; Nub polyfills them below.

const visible = owned.union(shared).difference(hidden);

if (required.isSubsetOf(visible) && blocked.isDisjointFrom(visible)) {
  render(visible);
}

The complete family is:

union  intersection  difference  symmetricDifference
isSubsetOf  isSupersetOf  isDisjointFrom

Map.prototype.getOrInsert()

The insertion methods return the value for a key, inserting one first if the key is absent. Native on Node 26+; Nub polyfills them below, on both Map and WeakMap.

const counts = new Map<string, number>();
counts.getOrInsert(word, 0);

// the callback runs only for a missing key
const result = cache.getOrInsertComputed(key, () => calculate(key));

Object.groupBy()

The grouping methods partition an iterable by a key the callback returns. Native on Node 21+; Nub polyfills them below.

const byStatus = Object.groupBy(tasks, (task) => task.status);
const byOwner = Map.groupBy(tasks, (task) => task.owner);

Array.prototype.toSorted()

The copying methods return an updated copy and leave the original array unchanged. Native on Node 20+; Nub polyfills them below.

const ranked = scores.toSorted((a, b) => b - a);
const reversed = values.toReversed();
const corrected = values.with(2, replacement);

// arrays only; typed arrays have no toSpliced
const withoutFirst = values.toSpliced(0, 1);

String.prototype.isWellFormed()

These methods detect or replace lone UTF-16 surrogates. Native on Node 20+; Nub polyfills them below.

if (!input.isWellFormed()) {
  input = input.toWellFormed(); // lone surrogates become U+FFFD
}

ArrayBuffer.prototype.transfer()

The transfer methods move a buffer's bytes into a new ArrayBuffer and detach the original. Native on Node 21+; Nub polyfills them below.

const source = new ArrayBuffer(1024);
const resized = source.transfer(2048);

console.log(source.detached); // true
console.log(resized.byteLength); // 2048

Array.fromAsync()

The Array.fromAsync() method collects an async iterable into an array. Native on Node 22+; Nub polyfills it below.

// the optional mapper runs on each value as it arrives
const rows = await Array.fromAsync(stream, (row) => normalize(row));

URL.parse()

The URL.parse() method returns null where the URL constructor would throw. Native on Node 20.18–20.x and 22.1+; Nub polyfills it elsewhere.

const callback = URL.parse(value, "https://example.com");
if (callback) console.log(callback.pathname);

RegExp.escape()

The RegExp.escape() method quotes a string for literal use in a regular expression. Native on Node 24+; Nub polyfills it below.

const exactName = new RegExp(`^${RegExp.escape(name)}$`, "u");

Error.isError()

The Error.isError() method identifies a real Error object without trusting its prototype. Native on Node 24+; Nub polyfills it below.

try {
  await runTask();
} catch (error) {
  if (Error.isError(error)) console.error(error.message);
}

Cross-realm errors on older Node

On Node 23 and below, the polyfill does not recognize an Error created in another realm, such as a node:vm context.

Float16Array

The half-precision float APIs store and round IEEE 754 binary16 values. Native on Node 24+; Nub polyfills them below.

const samples = new Float16Array([0.1, 0.2, 0.3]);
const rounded = Math.f16round(1 / 3);

const view = new DataView(new ArrayBuffer(2));
view.setFloat16(0, rounded);
console.log(view.getFloat16(0));

ArrayBuffer views on older Node

On Node 23 and below, ArrayBuffer.isView() returns false for a polyfilled Float16Array.

Uint8Array.prototype.toBase64()

The encoding methods convert bytes to and from base64, base64url, and hex. Native on Node 25+; Nub polyfills them below.

const bytes = new TextEncoder().encode("hello");
const encoded = bytes.toBase64({ alphabet: "base64url" });
const decoded = Uint8Array.fromBase64(encoded, { alphabet: "base64url" });
console.log(decoded.toHex()); // "68656c6c6f"

// decodes into an existing array; reports bytes read and written
const { read, written } = target.setFromHex("cafe");

The complete family is:

toBase64  fromBase64  setFromBase64  toHex  fromHex  setFromHex

DisposableStack

A DisposableStack collects cleanup callbacks and releases them in reverse order. Native on Node 24+; below it, Nub polyfills DisposableStack, AsyncDisposableStack, and SuppressedError.

const stack = new DisposableStack();
stack.defer(() => releaseLock());
stack.adopt(openHandle(), (handle) => handle.close());

try {
  await work();
} finally {
  stack.dispose(); // runs close(), then releaseLock()
}

Math.sumPrecise()

The Math.sumPrecise() method sums numbers without the rounding error and overflow that addition order causes. No Node release ships it yet; Nub polyfills it.

Math.sumPrecise([1, 1e100, 1, -1e100]); // 2

JSON.parse()

An options object as the second argument controls the objects that JSON.parse() creates. The freeze option deep-freezes every object and array in the result, and preferNullPrototype gives each object a null prototype and defaults to the value of freeze. Both must be booleans. The options proposal is at Stage 2.7. No Node release ships it yet; Nub polyfills it.

const config = JSON.parse(text, { freeze: true });
Object.isFrozen(config.servers[0]); // true
Object.getPrototypeOf(config); // null

// frozen, objects keep Object.prototype
JSON.parse(text, { freeze: true, preferNullPrototype: false });
// mutable, objects have a null prototype
JSON.parse(text, { preferNullPrototype: true });

Symbol.metadata

Decorators use Symbol.metadata as the key for class metadata. No Node release ships it yet; Nub polyfills it.

const metadata = Example[Symbol.metadata];

Atomics.pause()

The Atomics.pause() method hints to the CPU inside a short spin-wait loop. Native on Node 24+; below it, Nub's polyfill validates its argument and does nothing else.

while (Atomics.load(state, 0) === 0) {
  Atomics.pause();
}

Web APIs

These are browser-platform globals, such as URLPattern, WebSocket, and localStorage.

URLPattern

A URLPattern matches URL components and captures named groups. Native on Node 24+; Nub polyfills it below.

const route = new URLPattern({ pathname: "/users/:id" });
const match = route.exec("https://example.com/users/42");
console.log(match?.pathname.groups.id); // "42"

File

A File is a Blob with a name, media type, and modification time. Native on Node 20+; Nub polyfills it on Node 18 and 19.

const file = new File([JSON.stringify(payload)], "payload.json", {
  type: "application/json",
});

The global navigator reports runtime details such as hardwareConcurrency and userAgent. Native on Node 21+; Nub adds it below.

const workers = Math.max(1, navigator.hardwareConcurrency - 1);
console.log(navigator.userAgent, workers);

The Web Locks API serializes async access to a named resource within the process. Native on Node 24.5+; Nub polyfills it below.

await navigator.locks.request("database-migration", async () => {
  await migrate(); // one holder at a time
});

Worker

Nub adds a browser-style Worker global backed by node:worker_threads, with TypeScript entry points. Node ships no browser-style Worker, so Nub always adds one.

const worker = new Worker(new URL("./worker.ts", import.meta.url), {
  name: "thumbnailer",
});

worker.onmessage = ({ data }) => console.log(data);
worker.postMessage({ source: "photo.jpg" });

See Web Workers for lifecycle events, options, and Node interop.

localStorage

The storage globals are Node's built-in synchronous key/value store. Available on Node 22.4+; Nub passes the experimental flag before Node 25, and a storage file is still required.

localStorage.setItem("theme", "dark");
const theme = localStorage.getItem("theme");

sessionStorage.setItem("request-id", crypto.randomUUID());

See Web Storage for the storage file and scope.

reportError()

The reportError() function sends an error to the uncaught-error path without throwing it. No Node release ships it yet; Nub polyfills it.

backgroundTask().catch((error) => {
  reportError(error);
});

WebSocket

The WebSocket global opens a client connection. Native on Node 22+; Nub passes Node's experimental flag on 20.10–21.x.

const socket = new WebSocket("wss://example.com/events");
socket.addEventListener("message", ({ data }) => console.log(data));

EventSource

An EventSource consumes a server-sent event stream. Available on Node 20.18–20.x and 22.3+, where Nub passes Node's experimental flag.

const events = new EventSource("https://example.com/events");
events.addEventListener("update", ({ data }) => console.log(data));

MessageEvent.ports

A message event exposes transferred ports as a read-only ports array. Node makes it read-only from 22.3; Nub patches older versions to match.

channel.port1.onmessage = (event) => {
  const [transferredPort] = event.ports;
  transferredPort?.postMessage("received");
};

Node.js APIs and module loading

Most of these entries turn on features that a given Node release already contains, and Nub does not emulate those on a Node that lacks them. The node:util rate helpers are plain JavaScript, so Nub polyfills them.

util.debounce() and util.throttle()

util.debounce() delays a call until activity stops, and util.throttle() limits calls within an interval and queues the excess. Both return promises and expose cancellation, pending-call state, and timer controls. Native on Node 24.22–24.x and 26.10+; on other versions Nub polyfills both, including named node:util imports and compiled artifacts.

import { debounce, throttle } from "node:util";

const save = debounce((value: string) => value, 100);
const send = throttle((value: string) => value, 2, 1000);
await Promise.all([save("draft"), send("event")]);

vm.Module

The VM module classes compile and link ES modules inside a VM context. No Node release has unflagged them, so Nub always passes --experimental-vm-modules.

import vm from "node:vm";

const module = new vm.SourceTextModule("export const answer = 42");
await module.link(() => { throw new Error("no imports expected"); });
await module.evaluate();

WebAssembly module imports

A .wasm file imports through the ESM loader. Native on Node 22.19–22.x and 24.5+; Nub passes the experimental flag on the rest of the supported range.

import { add } from "./math.wasm";

console.log(add(20, 22)); // 42

node:sqlite

Node's built-in SQLite module gives synchronous database access with no npm dependency. Available on Node 22.5+; Nub passes the experimental flag before 22.13 and 23.4.

import { DatabaseSync } from "node:sqlite";

const database = new DatabaseSync("app.db");
const row = database.prepare("SELECT count(*) AS count FROM users").get();

AsyncLocalStorage

Node 24 rebuilt AsyncLocalStorage on V8's context frames, which roughly halves the cost of carrying a store across each await. Nub passes --experimental-async-context-frame to turn it on for Node 22.9–23.x.

import { AsyncLocalStorage } from "node:async_hooks";

const requestId = new AsyncLocalStorage<string>();

// every hop below carries the store; that propagation got cheaper
await requestId.run("req-1", async () => {
  await fetchUser();
  console.log(requestId.getStore()); // "req-1"
});

Native addon imports

A native .node addon imports directly from an ES module. Available on Node 22.20–22.x and 23.6+, where Nub passes --experimental-addon-modules.

import addon from "./build/Release/addon.node";

console.log(addon.version());

Text imports

The type: "text" import attribute loads a file as a string, the shape Node calls text modules. Nub uses Node's flagged implementation on 24.19–24.x and 26.5+, and its own loader hook elsewhere from Node 18.20.

import template from "./email.html" with { type: "text" };

console.log(template.length);

node:stream/iter

The stream iterator module provides iterator-oriented stream utilities. Available on Node 25.9+, where Nub passes --experimental-stream-iter.

import { from, text } from "node:stream/iter";

console.log(await text(from("Hello, world!")));

node:ffi

The FFI module calls functions in native shared libraries. Available on Node 26.1+, where Nub passes --experimental-ffi; permission flags stay the application's choice.

import { dlopen } from "node:ffi";

const { lib, functions } = dlopen("./libmath.so", {
  add_i32: { arguments: ["i32", "i32"], return: "i32" },
});

console.log(functions.add_i32(20, 22)); // 42
lib.close();

node:vfs

The VFS module exposes Node's virtual filesystem primitives. Available on Node 26.4+, where Nub passes --experimental-vfs.

import vfs from "node:vfs";

const memory = vfs.create();
memory.mkdirSync("/data", { recursive: true });
memory.writeFileSync("/data/message.txt", "hello");
console.log(memory.readFileSync("/data/message.txt", "utf8"));

Deferred module evaluation

The import defer form loads a module but runs it only on first property access. Available on Node 26.4+; Nub turns on V8's flag the first time it loads a module that uses the syntax.

import defer * as analytics from "./analytics.ts";

// nothing in analytics.ts has run yet
if (consented) {
  analytics.track("start"); // the module evaluates here, once
}

Avoid the dynamic form

Once a program uses static import defer, a later import.defer(specifier) call crashes the process with a V8 fatal error instead of throwing a SyntaxError. The crash is a Node defect that the flag exposes. Use the static form.

Module syntax detection

Syntax detection runs an ambiguous .js file as ESM when it contains module syntax. Native on Node 20.19–20.x and 22.7+; Nub passes the flag on 20.10–20.18 and 21.1–22.6.

// package.json has no "type" field; Node detects this file as ESM
export const answer = 42;

How Nub supplies these

Every mechanism below is one Node itself provides, and a native implementation always wins.

  • Polyfills run from Nub's preload and define a global or method only when it is missing.
  • Lazy globals defer a large polyfill, such as Temporal, until code first reads it.
  • Semantic patches correct a narrow behavior where an API exists but does not yet match the standard.
  • Experimental flags turn on an API already built into the running Node release, chosen per exact version.
  • Loader hooks add import formats through Node's module-loader API.

Compatibility mode turns all of this off.