Nub fills gaps between supported Node versions so application code can use one modern API surface. The reference below describes what Nub adds, how it is delivered, and when Node's native implementation takes over.

TypeScript setup

Nub's types package declares the keyed Promise combinators, Temporal, the browser-style Worker global, and reportError. 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"] } }

How augmentation works

Nub augments the stock Node binary through Node's extension surfaces. It does not patch Node, replace V8, or ship a fork.

  • JavaScript polyfills run from Nub's preload and feature-detect each property before defining it. They fill missing globals and methods without replacing native or user-supplied implementations.
  • Lazy globals defer larger fallbacks until first use. On older Node versions, reading Temporal loads the fallback and replaces the getter with the resulting namespace.
  • Semantic patches correct a narrow behavior on versions where an API exists but does not yet match the standard. They are also feature-detected.
  • Experimental flags expose APIs already built into a particular Node release. Nub derives flags from the exact Node version because passing an unknown flag would abort Node during startup.
  • Loader hooks add supported import formats through Node's module-loader API. They do not emulate native subsystems that are absent from the running Node.

Native implementations always win. As Node adds an API, Nub's feature check steps aside; application code keeps the same surface while the delivery mechanism changes underneath it.

Compatibility mode disables the entire augmentation layer. Run with nub --node or set NODE_COMPAT=1 to use the project's pinned Node with no polyfills, preload patches, loader hooks, or automatically enabled flags.

Reference

Availability below covers Nub's supported Node range, beginning with Node 18.19. A native-version note says when Nub can step aside, not when application code may first use the API under Nub.

JavaScript APIs

Temporal

Temporal provides immutable types for exact instants, calendar values, durations, and time-zone-aware values. Date.prototype.toTemporalInstant() converts an existing Date to the equivalent instant.

Delivery

Lazy global polyfill on Node 18–25; native on Node 26+. Nub loads @js-temporal/polyfill only when code first reads Temporal and routes direct imports of that package to the same namespace.

const deployed = Temporal.Instant.from("2026-08-07T16:30:00Z");
const local = deployed.toZonedDateTimeISO("America/Los_Angeles");
const fromDate = new Date("2026-08-07T16:30:00Z").toTemporalInstant();

console.log(local.add({ days: 1 }).toString());
console.log(fromDate.equals(deployed)); // true

Promise.allKeyed()

The keyed Promise combinators accept an object and return a null-prototype object with the same own enumerable string and symbol keys. Values pass through Promise.resolve; allKeyed rejects on the first rejection, while allSettledKeyed records every outcome.

Delivery

Feature-detected JavaScript polyfill on every supported Node version. No Node release ships the await-dictionary proposal yet.

const { account, invoices } = await Promise.allKeyed({
  account: getAccount(accountId),
  invoices: listInvoices(accountId),
});

const outcomes = await Promise.allSettledKeyed({
  profile: getProfile(account.id),
  avatar: getAvatar(account.id),
});

Promise.withResolvers()

Promise.withResolvers() creates a promise with its resolve and reject functions. Promise.try() invokes a callback and turns either its return value or thrown error into a promise outcome.

Delivery

Feature-detected JavaScript polyfills. Promise.withResolvers() is native on Node 22+; Promise.try() is native on Node 24+.

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

const config = await Promise.try(() => JSON.parse(source));
console.log(await promise, config);

Iterator helpers

Iterator helpers transform and consume iterators lazily without first materializing an array.

Delivery

Feature-detected JavaScript polyfill before Node 22; native on Node 22+.

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

Helper identity on older Node

The fallback returns generator-backed helper objects. Iteration, laziness, validation, and iterator closing match the proposal, but the prototype and Symbol.toStringTag differ from native iterator-helper objects.

The supplied helpers are map, filter, take, drop, flatMap, reduce, toArray, some, every, find, and forEach.

Iterator.concat()

Iterator.concat() lazily visits each iterable in sequence.

Delivery

Feature-detected JavaScript polyfill before Node 26; native on Node 26+.

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

Iterator.zip()

The joint-iteration methods advance several iterables together. zip() yields arrays, while zipKeyed() preserves the keys of an input object.

Delivery

Feature-detected JavaScript polyfill on every supported Node version. No Node release ships these methods yet.

const pairs = Iterator.zip([ids, names]).toArray();

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

Iterator.prototype.chunks()

These helpers batch an iterator, produce overlapping windows, search for a value, or join values into a string.

Delivery

Feature-detected JavaScript polyfill on every supported Node version. No Node release ships these methods yet.

const batches = Iterator.from(records).chunks(100).toArray();
const movingTriples = Iterator.from(values).windows(3).toArray();
const hasDraft = Iterator.from(statuses).includes("draft");
const label = Iterator.from(tags).join(", ");

Set.prototype.union()

The Set methods provide standard union, intersection, difference, relationship, and overlap operations.

Delivery

Feature-detected JavaScript polyfill before Node 22; native on Node 22+.

const visible = owned.union(shared).difference(hidden);
const overlap = selected.intersection(available);

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

The complete family is union, intersection, difference, symmetricDifference, isSubsetOf, isSupersetOf, and isDisjointFrom.

Map.prototype.getOrInsert()

The insertion methods return an existing value or insert one when the key is absent. The computed form calls its callback only for a missing key. Both methods are also available on WeakMap.

Delivery

Feature-detected JavaScript polyfill before Node 26; native on Node 26+.

const cache = new Map<string, Result>();
const result = cache.getOrInsertComputed(key, () => calculate(key));

Object.groupBy()

The grouping methods partition an iterable by a key returned from a callback.

Delivery

Feature-detected JavaScript polyfill before Node 21; native on Node 21+.

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

Array.prototype.toSorted()

The copying methods produce an updated array without mutating the original. Typed arrays support toSorted, toReversed, and with, but not toSpliced.

Delivery

Feature-detected JavaScript polyfill before Node 20; native on Node 20+.

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

String.prototype.isWellFormed()

These methods detect or replace lone UTF-16 surrogates.

Delivery

Feature-detected JavaScript polyfill before Node 20; native on Node 20+.

if (!input.isWellFormed()) {
  input = input.toWellFormed();
}

ArrayBuffer.prototype.transfer()

ArrayBuffer transfer moves the bytes into a new buffer and detaches the original.

Delivery

Feature-detected JavaScript polyfill before Node 21; native on Node 21+.

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

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

Array.fromAsync()

Array.fromAsync() builds an array from an async iterable, optionally mapping each value as it arrives.

Delivery

Feature-detected JavaScript polyfill before Node 22; native on Node 22+.

const rows = await Array.fromAsync(stream, (row) => normalize(row));

URL.parse()

URL.parse() returns a parsed URL or null instead of throwing for invalid input.

Delivery

Feature-detected JavaScript polyfill on Node versions without the method. It is native on Node 20.19–20.x and Node 22.1+.

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

RegExp.escape()

RegExp.escape() quotes a string for literal use inside a regular expression.

Delivery

Feature-detected JavaScript polyfill before Node 24; native on Node 24+.

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

Error.isError()

Error.isError() identifies Error objects without relying on a mutable prototype property.

Delivery

Feature-detected JavaScript polyfill before Node 24; native on Node 24+.

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 userland fallback cannot recognize an Error created in another realm because V8's internal Error slot is not exposed to JavaScript.

Float16Array

The half-precision float APIs store and round IEEE 754 binary16 values.

Delivery

Feature-detected JavaScript polyfill before Node 24; native on Node 24+.

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() cannot recognize a polyfilled Float16Array because JavaScript cannot create V8's internal typed-array slot.

Uint8Array.prototype.toBase64()

The encoding methods convert bytes to and from base64, base64url, and hexadecimal text. The setFrom variants decode into an existing array and report how much input was read and how many bytes were written.

Delivery

Feature-detected JavaScript polyfill before Node 25; native on Node 25+.

const bytes = new TextEncoder().encode("hello");
const encoded = bytes.toBase64({ alphabet: "base64url", omitPadding: true });
const decoded = Uint8Array.fromBase64(encoded, { alphabet: "base64url" });

console.log(decoded.toHex());

The complete family is toBase64, fromBase64, setFromBase64, toHex, fromHex, and setFromHex.

DisposableStack

Explicit Resource Management collects cleanup callbacks and disposable resources, then releases them in reverse order.

Delivery

Feature-detected JavaScript polyfill before Node 24; native on Node 24+.

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

try {
  await work();
} finally {
  stack.dispose();
}

Math.sumPrecise()

Math.sumPrecise() reduces floating-point error and avoids overflow caused only by an unfortunate addition order.

Delivery

Feature-detected JavaScript polyfill on every supported Node version. No Node release ships this method yet.

const total = Math.sumPrecise([1, 1e100, 1, -1e100]);
console.log(total); // 2

Symbol.metadata

Decorators use Symbol.metadata as the key for class metadata.

Delivery

Feature-detected JavaScript polyfill on every supported Node version. No Node release ships this symbol yet.

const metadata = Example[Symbol.metadata];

Atomics.pause()

Atomics.pause() provides a hint inside short spin-wait loops.

Delivery

Feature-detected JavaScript polyfill on every supported Node version. No Node release ships this method yet; the fallback validates its argument and otherwise acts as a no-op.

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

Web APIs

URLPattern

URLPattern matches URL components and exposes named groups.

Delivery

Feature-detected global polyfill before Node 24; native on Node 24+.

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

File

File represents named Blob data with a media type and modification time.

Delivery

Feature-detected global polyfill on Node 18 and 19; native on Node 20+.

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

The global navigator exposes server-relevant runtime information such as hardwareConcurrency and userAgent.

Delivery

Feature-detected global shim before Node 21; native on Node 21+. Nub adds only missing properties and preserves any existing navigator object.

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

The Web Locks API coordinates asynchronous access to a named resource within the process.

Delivery

Feature-detected Web Locks implementation before Node 24.5; native on Node 24.5+.

await navigator.locks.request("database-migration", async () => {
  await migrate();
});

Worker

Nub exposes a browser-style Worker global backed by Node worker threads, including web events, message channels, transfer lists, TypeScript entrypoints, and worker names.

Delivery

Preload-installed global on every supported Node version. Node does not ship a browser-style global Worker; Nub adapts node:worker_threads without replacing that module.

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, worker options, module loading, and Node interop.

localStorage

The storage globals provide synchronous string key/value storage using Node's built-in Web Storage implementation.

Delivery

Version-gated Node API available on Node 22.4+. Nub enables the experimental flag before Node 25, but a storage file is still required; Nub does not emulate the API on older Node.

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

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

See Web Storage for configuring the storage file and choosing a scope.

reportError()

reportError() reports an exception through the host's uncaught-error path without throwing it at the call site.

Delivery

Feature-detected global polyfill on every supported Node version. No Node release ships this global yet.

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

WebSocket

The WebSocket global creates standards-compatible client connections.

Delivery

Version-gated Node API available on Node 20.10+. Nub enables Node's experimental implementation before Node 22; it is native without a flag on Node 22+. Nub does not emulate it on earlier releases.

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

EventSource

EventSource consumes a server-sent event stream.

Delivery

Version-gated Node API available on Node 20.18–20.x and Node 22.3+. It is unavailable on Node 21 and remains experimental through Node 26.

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

MessageEvent.ports

Transferred message ports are exposed through a read-only array, matching the web-platform contract.

Delivery

Semantic preload patch on older Node versions whose MessageEvent.ports value is mutable. Nub leaves conforming native behavior unchanged.

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

Node.js APIs and loading behavior

These entries expose parsers, modules, and flags already present in particular Node releases. Nub does not polyfill a missing native subsystem.

vm.Module

The VM module classes compile and link ECMAScript modules inside a VM context.

Delivery

Experimental-flag enablement on every supported Node version. Nub passes --experimental-vm-modules automatically.

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

WebAssembly modules can be imported through the ESM loader.

Delivery

Node feature enabled automatically across the supported range. Nub passes the experimental flag on Node 18.19–22.18 and 23.0–24.4; the feature is unflagged on Node 22.19–22.x and Node 24.5+.

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

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

node:sqlite

Node's built-in SQLite module provides synchronous database access without an npm dependency.

Delivery

Version-gated Node API available on Node 22.5+. Nub enables its experimental flag before Node 22.13 and 23.4; it is unflagged afterward.

import { DatabaseSync } from "node:sqlite";

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

Native addon imports

Native .node addons can be imported directly from an ES module.

Delivery

Version-gated Node API available on Node 22.20–22.x and Node 23.6+. Nub enables --experimental-addon-modules; the feature remains experimental through Node 27.

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

console.log(addon.version());

Text imports

The type: "text" import attribute loads a file as a string.

Delivery

Loader-hook fallback from Node 18.20 onward, or Node's flagged implementation on Node 24.19–24.x and 26.5+. Nub uses the native parser where available.

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

console.log(template.length);

node:stream/iter

The stream iterator module provides iterator-oriented stream utilities.

Delivery

Version-gated Node API available on Node 25.9+. Nub enables --experimental-stream-iter; it does not emulate the module on older releases.

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

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

node:ffi

The FFI module calls functions in native dynamic libraries.

Delivery

Version-gated Node API available on Node 26.1+. Nub enables --experimental-ffi; permission flags remain 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));
lib.close();

node:vfs

The VFS module exposes Node's virtual filesystem primitives.

Delivery

Version-gated Node API available on Node 26.4+. Nub enables --experimental-vfs; it does not emulate the module on older releases.

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"));

Module syntax detection

Syntax detection lets Node classify ambiguous .js input as ESM when it contains module syntax.

Delivery

Version-gated Node behavior available on Node 20.10+ except Node 21.0. Nub enables the flag before it became default in Node 20.19 and 22.7.

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