Globals & Web Platform APIs
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Node.js 26 (26.10.0, which becomes LTS on 2026-10-28), with the differences from Node.js 24 (24.21.0) and Node.js 22 (22.23.3) called out, and was checked on 2026-10-03. The documentation page for the global objects is marked Stability 2 (Stable) as a whole, but individual globals carry their own level: URLPattern is Experimental, Web Storage is a Release candidate, navigator is Active development and Temporal is a JavaScript language feature that comes from V8 rather than from Node.js. Every example was run on all three lines, and the page says where the output differs.
Node.js began as a server-side runtime with its own vocabulary (process, Buffer, setImmediate, require) and has spent the last decade adding the browser’s vocabulary on top of it: fetch, URL, AbortController, TextEncoder, Blob, structuredClone, performance and more. The practical benefit is that code written against Web standards runs unchanged in the browser, in Node.js and in other server runtimes. This page tours that shared layer and the Node.js-specific globals next to it, then explains the stability index once, because every other page in this section labels APIs with it.
The global scope and globalThis
In a browser, a top-level var creates a property of the global object. Node.js is different: every file is a module (an ES module or a CommonJS module), so top-level declarations are local to the file and never leak onto the global object. The global object itself is reachable as globalThis, the standard name defined by the ECMAScript language and available in every modern runtime. The older name global is a Node.js-only alias of the same object and is covered under Legacy globals.
Several names look global but are not: require, module, exports, __filename and __dirname are parameters of the CommonJS module wrapper, so they exist in CommonJS files only. In ES modules use import.meta.filename and import.meta.dirname instead (see CommonJS modules and ECMAScript modules).
console.log(globalThis === global); // true
console.log(typeof window, typeof self, typeof document); // undefined undefined undefined
var leaked = 1; // module scope, not a global
console.log('leaked' in globalThis); // false
globalThis.appName = 'Bookshelf'; // explicit global: visible in every module
console.log(appName); // Bookshelf
Source: Node.js API: Global objects
Writing to globalThis is the one way to create a real global, and it is almost always a mistake in application code: any module can read or overwrite the value, tests leak state into each other and the dependency becomes invisible. Reserve it for polyfills that must be present before other code runs, and prefer a module export otherwise.
A freshly started Node.js 26 process exposes these lower-case names (language built-ins such as parseInt are included in the list):
const names = Object.getOwnPropertyNames(globalThis).filter((n) => /^[a-z]/.test(n));
console.log(names.join(' '));
parseFloat parseInt undefined globalThis decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape eval isFinite isNaN console process global clearImmediate setImmediate clearInterval clearTimeout setInterval setTimeout queueMicrotask structuredClone atob btoa performance fetch crypto navigator localStorage sessionStorage
Source: Node.js API: Global objects
Node.js 24 prints the same list without localStorage and sessionStorage (Web Storage is off by default there). Node.js 22 prints the same names as Node.js 24; the only difference is the order of the last two, navigator crypto instead of crypto navigator. Classes (upper-case names such as URL, Blob or AbortController) are global too; the API page lists them all.
| Group | Examples | Implemented by |
|---|---|---|
ECMAScript language built-ins |
|
V8 |
Node.js specific |
|
Node.js core |
Web Platform APIs |
|
Node.js core, with |
The timers (setTimeout, setInterval, setImmediate) are described in Timers, Buffer in Buffers & binary data, and the Web Streams classes (ReadableStream, TransformStream, CompressionStream and the rest) in Web streams & interop. Several of those became Stable together in Node.js 23.11 and 22.15.
fetch, Headers, Request and Response
fetch() is a global since Node.js 18 (behind a flag from 17.5) and has been stable since Node.js 21. Together with Headers, Request, Response and FormData it follows the WHATWG Fetch standard, so the code you write for the browser works here. The implementation is undici, an HTTP/1.1 client written from scratch for Node.js; process.versions.undici reports the bundled version (8.10.2 on Node.js 26.10.0, 7.29.1 on Node.js 24.21.0 and 6.28.1 on Node.js 22.23.3). fetch() accepts a custom dispatcher option for proxies and connection pools; that, plus error handling, retries and timeouts, is the subject of Fetch & HTTP clients. This section only shows the global itself.
import { createServer } from 'node:http';
const server = createServer((req, res) => {
res.setHeader('content-type', 'application/json');
res.end(JSON.stringify({ path: req.url, method: req.method }));
}).listen(0);
await new Promise((resolve) => server.once('listening', resolve));
const { port } = server.address();
const res = await fetch(`http://127.0.0.1:${port}/books?limit=2`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ title: 'Dune' }),
signal: AbortSignal.timeout(2000), // abort after 2 s
});
console.log(res.status, res.ok, res.headers.get('content-type'));
console.log(await res.json());
try {
await fetch('http://127.0.0.1:1/', { signal: AbortSignal.timeout(500) });
} catch (e) {
console.log(e.name, e.message); // network failures reject
}
server.close();
200 true application/json
{ path: '/books?limit=2', method: 'POST' }
TypeError fetch failed
Source: Node.js Learn: Fetch
Two behaviours surprise newcomers. A fetch() promise rejects only on network-level failures (DNS errors, refused connections, aborts); an HTTP 404 or 500 resolves normally, so check res.ok or res.status. And a response body is a stream that can be consumed once: call exactly one of res.json(), res.text() or res.arrayBuffer(), or res.clone() first.
URL, URLSearchParams and URLPattern
URL and URLSearchParams are globals that implement the WHATWG URL standard, the same parser browsers use. They replace the legacy url.parse() API of the node:url module, which is deprecated for application code (DEP0169, an application deprecation since Node.js 24, documentation-only before that) because it parses some inputs differently from browsers. URL.canParse() (Node.js 18.17 and 19.9) answers whether an input is valid without throwing, and URL.parse() (Node.js 22.1) returns null instead of throwing. Path and URL helpers are covered in OS, path & URL.
const u = new URL('/api/books?genre=sf&page=2#top', 'https://example.org');
u.searchParams.append('sort', 'title');
u.searchParams.set('page', '3');
console.log(u.href);
console.log(u.pathname, u.search, [...u.searchParams.keys()]);
console.log(URL.canParse('nope'), URL.canParse('/x', 'https://a.b'));
console.log(URL.parse('nope'));
https://example.org/api/books?genre=sf&page=3&sort=title#top
/api/books ?genre=sf&page=3&sort=title [ 'genre', 'page', 'sort' ]
false true
null
Source: Node.js API: URL
URLPattern matches URLs, or parts of them, against a pattern syntax with named groups and optional regular expressions, much like a router would. It is the standard behind the pattern matching of service workers and is useful for routers and allow-lists. As a global it is Stability 1 (Experimental) and was added in Node.js 24.0.0, so it exists on Node.js 24 and 26 and is undefined on Node.js 22 (the node:url page lists the class as added in 23.8.0, where it had to be imported). Experimental means the API may still change; do not build a public library on it.
const pattern = new URLPattern({ pathname: '/books/:id(\\d+)' });
console.log(pattern.test('https://example.org/books/42')); // true
console.log(pattern.exec('https://example.org/books/42').pathname.groups); // { id: '42' }
console.log(pattern.test('https://example.org/books/dune')); // false (not digits)
Source: Node.js API: URL (URLPattern)
On Node.js 22 the same snippet throws ReferenceError: URLPattern is not defined; feature-detect with typeof URLPattern === 'function' if you must support it.
AbortController and AbortSignal
AbortController and AbortSignal are the standard cancellation mechanism. You create a controller, pass its signal to an operation and call abort() when the result is no longer wanted. Node.js accepts a signal option in fetch(), timers (node:timers/promises), node:fs/promises, node:child_process, node:events and many streams APIs. AbortSignal.timeout(ms) (17.3) creates a signal that aborts on its own, AbortSignal.any([…]) (20.3) combines signals, signal.reason carries the cause (17.2) and signal.throwIfAborted() turns a cancelled state into an exception for hand-written loops. The full cancellation story, including propagating signals through layers, is in Async context & cancellation.
import { setTimeout as sleep } from 'node:timers/promises';
const ac = new AbortController();
ac.signal.addEventListener('abort', () => console.log('aborted:', ac.signal.reason.message), { once: true });
const pending = sleep(5000, null, { signal: ac.signal })
.catch((e) => console.log(e.name, e.code));
setTimeout(() => ac.abort(new Error('user cancelled')), 10);
await pending;
const any = AbortSignal.any([ac.signal, AbortSignal.timeout(20)]);
console.log(any.aborted, any.reason.message); // already aborted by ac
try { ac.signal.throwIfAborted(); } catch (e) { console.log('throwIfAborted:', e.message); }
aborted: user cancelled
AbortError ABORT_ERR
true user cancelled
throwIfAborted: user cancelled
A note on timing: the abort event fires only once, and an already-aborted signal never fires it again, so check signal.aborted before registering a listener. A documentation-only deprecation (DEP0209, Node.js 26.8.2) discourages using an AbortSignal to dispose of resources in favour of the using declaration.
structuredClone
structuredClone(value, options) (Node.js 17) makes a deep copy using the structured clone algorithm, the same one used by postMessage, MessageChannel and worker_threads. Unlike the JSON.parse(JSON.stringify(x)) idiom it preserves Date, Map, Set, RegExp, typed arrays, BigInt and circular references, and it can transfer an ArrayBuffer instead of copying it. It cannot clone functions (it throws a DataCloneError) and it does not preserve class prototypes, so class instances come back as plain objects.
const original = { title: 'Dune', tags: new Set(['sf']), added: new Date(0), nested: { n: 1n } };
const copy = structuredClone(original);
console.log(copy, copy.tags !== original.tags);
const cyc = { name: 'loop' }; cyc.self = cyc;
console.log(structuredClone(cyc).self.name); // loop
try { structuredClone({ f() {} }); } catch (e) { console.log(e.name, e.constructor.name); }
class Book { constructor(t) { this.title = t; } hi() { return 1; } }
const b = structuredClone(new Book('x'));
console.log(b instanceof Book, b.hi); // false undefined
const buf = new ArrayBuffer(8);
const moved = structuredClone(buf, { transfer: [buf] });
console.log(buf.byteLength, moved.byteLength); // 0 8 (the original is detached)
{
title: 'Dune',
tags: Set(1) { 'sf' },
added: 1970-01-01T00:00:00.000Z,
nested: { n: 1n }
} true
loop
DataCloneError DOMException
false undefined
0 8
queueMicrotask
queueMicrotask(callback) (Node.js 11) schedules a function on the V8 microtask queue, the queue that runs promise reactions. It runs after the current synchronous code and before the event loop moves on. If the callback throws, the error surfaces as an uncaughtException on process. Node.js also has process.nextTick(), whose queue is drained before the microtask queue; the Node.js documentation marks nextTick as Stability 3 (Legacy, since Node.js 22.7 and 20.18) and recommends queueMicrotask() instead. The exact ordering depends on where the code runs, as the example shows: in an ES module the whole file already executes inside a promise job, so microtasks go first, whereas in a CommonJS main script nextTick goes first.
const order = [];
queueMicrotask(() => order.push('queueMicrotask'));
Promise.resolve().then(() => order.push('promise'));
process.nextTick(() => order.push('nextTick'));
order.push('sync');
await new Promise((resolve) => setTimeout(resolve, 10));
console.log(order.join(' > '));
process.once('uncaughtException', (err) => console.log('uncaughtException:', err.message));
queueMicrotask(() => {
throw new Error('boom');
});
sync > queueMicrotask > promise > nextTick
uncaughtException: boom
Saved as a CommonJS file (with the await replaced by a setTimeout callback) the first line becomes sync > nextTick > queueMicrotask > promise; the result was identical on Node.js 22, 24 and 26. The full picture of the queues is in the event loop page. As a rule, use queueMicrotask() when you need to defer work to "right after this synchronous code" and never recurse into it forever: a microtask that always queues another microtask starves I/O.
TextEncoder and TextDecoder
TextEncoder and TextDecoder (global since Node.js 11) convert between strings and UTF-8 bytes following the WHATWG Encoding standard. TextEncoder always produces UTF-8 and offers encodeInto() to write into an existing buffer without allocating. TextDecoder takes a label (UTF-8 by default, plus UTF-16 and a number of other encodings when Node.js is built with full ICU, the default for official builds), and two options matter: fatal: true makes invalid input throw instead of inserting the replacement character, and decode(chunk, { stream: true }) handles multi-byte characters split across chunks. The Node.js-specific alternative for strings and bytes is Buffer, covered in Buffers & binary data; prefer TextEncoder/TextDecoder in code that should also run in the browser.
const bytes = new TextEncoder().encode('Café ☕');
console.log(bytes.length); // 9
console.log(new TextDecoder().decode(bytes)); // Café ☕
const strict = new TextDecoder('utf-8', { fatal: true });
try { strict.decode(new Uint8Array([0xff, 0xfe])); } catch (e) { console.log(e.name, e.code); }
console.log(new TextDecoder().decode(new Uint8Array([0xff, 0x41]))); // replacement char, then A
const decoder = new TextDecoder();
const euro = new TextEncoder().encode('€'); // 3 bytes
console.log(JSON.stringify(decoder.decode(euro.slice(0, 2), { stream: true }) + decoder.decode(euro.slice(2))));
const { written } = new TextEncoder().encodeInto('Dune', new Uint8Array(3));
console.log(written); // 3: the buffer was too small
9
Café ☕
TypeError ERR_ENCODING_INVALID_ENCODED_DATA
�A
"€"
3
The matching stream classes TextEncoderStream and TextDecoderStream have been Stable since Node.js 23.11 and 22.15.
Blob, File and FormData
Blob (global since Node.js 18) is an immutable chunk of raw data with a MIME type. It can be sliced without copying, read with text() or arrayBuffer(), turned into a ReadableStream with stream() and sent through postMessage. File (a Blob with a name and a modification time) and FormData are globals too, and they are what fetch() uses for multipart uploads. Use Blob when the data should travel between threads or into fetch; use Buffer for general server-side byte handling.
const blob = new Blob(['id,title\n', '1,Dune\n'], { type: 'text/csv' });
console.log(blob.size, blob.type); // 16 text/csv
console.log(await blob.slice(0, 2).text()); // id
const file = new File([blob], 'books.csv', { type: 'text/csv', lastModified: 0 });
console.log(file.name, file.size);
const form = new FormData();
form.set('upload', file);
form.set('note', 'first import');
console.log(new Response(form).headers.get('content-type').split(';')[0]);
const chunks = [];
for await (const c of blob.stream()) chunks.push(c.byteLength);
console.log(chunks);
16 text/csv
id
books.csv 16
multipart/form-data
[ 9, 7 ]
Source: Node.js API: Buffer (Blob, File)
MessageChannel and BroadcastChannel
MessageChannel creates two entangled MessagePort objects, and a message posted on one is delivered to the other after being copied with the structured clone algorithm (Node.js 15). It is the building block that worker_threads uses, and it also works inside a single thread. BroadcastChannel (Node.js 18) is a named channel: every BroadcastChannel with the same name, in the same process (the main thread and its workers), receives each message except the sender’s own. Neither crosses process boundaries; for that use IPC on child processes, a socket or a message broker. Workers are covered in Worker threads.
const { port1, port2 } = new MessageChannel();
port2.onmessage = ({ data }) => { console.log('port2 got', data); port2.close(); };
port1.postMessage({ book: 'Dune', tags: new Set(['sf']) }); // structured clone, so a Set works
const a = new BroadcastChannel('catalog');
const b = new BroadcastChannel('catalog');
b.onmessage = (e) => { console.log('b got', e.data); a.close(); b.close(); };
a.postMessage('book-added');
port2 got { book: 'Dune', tags: Set(1) { 'sf' } }
b got book-added
An open port keeps the event loop alive, which is why the example closes both channels; forgetting to do so is the usual reason a script never exits.
performance
The global performance is the perf_hooks.performance object (Node.js 16), part of the Performance measurement APIs, which are Stability 2 (Stable). performance.now() returns a monotonic high-resolution time in milliseconds relative to performance.timeOrigin, which makes it the right clock for measuring durations (unlike Date.now(), it does not jump when the system clock changes). performance.mark() and performance.measure() record named entries, and a PerformanceObserver receives them asynchronously. Classes such as PerformanceEntry, PerformanceMark and PerformanceObserver are globals since Node.js 19. Profiling, event-loop delay histograms and timerify() are in Profiling & performance.
performance.mark('start');
let sum = 0; for (let i = 0; i < 1e6; i++) sum += i;
performance.mark('end');
const m = performance.measure('loop', 'start', 'end');
console.log(m.name, typeof m.duration, m.duration >= 0);
const obs = new PerformanceObserver((list, o) => {
for (const e of list.getEntries()) console.log('observed', e.entryType, e.name);
o.disconnect();
});
obs.observe({ entryTypes: ['measure'] });
performance.measure('again', 'start', 'end');
loop number true
observed measure again
navigator
navigator (Node.js 21) is a deliberately partial implementation of the browser Navigator and is Stability 1.1 (Active development); it can be removed with --no-experimental-global-navigator. It exposes hardwareConcurrency (the number of logical processors, a sensible default for sizing a worker pool), language and languages (from ICU), platform and userAgent (the string is Node.js/ followed by the major version, for example Node.js/26). navigator.locks (Node.js 24.5, Stability 1 Experimental) is the Web Locks API: it coordinates access to a named resource across the threads of one process, so it is not available on Node.js 22.
console.log(navigator.hardwareConcurrency > 0); // true
console.log(navigator.userAgent); // Node.js/26
console.log(navigator.platform, navigator.language, navigator.languages);
await navigator.locks.request('catalog-import', async (lock) => {
console.log('holding', lock.name, lock.mode); // exclusive by default
}); // released on return
true
Node.js/26
MacIntel en-US [ 'en-US' ]
holding catalog-import exclusive
The platform and language values depend on the machine that ran the example. On Node.js 22 the first three lines work and the navigator.locks call throws a TypeError. Do not use userAgent for feature detection: check for the feature itself.
Web Storage: localStorage and sessionStorage
Web Storage (localStorage, sessionStorage and the Storage class) arrived in Node.js 22.4 behind --experimental-webstorage and has been enabled by default since Node.js 25.0.0, as Stability 1.2 (Release candidate). It follows the browser API: synchronous getItem, setItem, removeItem, clear, key and length, with string values only and a quota of 10 MB. The two objects are very different on a server:
-
sessionStoragelives in memory and is shared only within the running process (not between workers). -
localStorageis persisted to the file given with--localstorage-file. The data is stored unencrypted, and on a server it is not per user or per request: every user and every request sees the same data. Treat it as a small process-wide settings file, never as session or user storage.
Without --localstorage-file, localStorage is unavailable. The Node.js 26 documentation says that reading the global then throws a DOMException (changed in 26.0.0; in 25.x it returned an empty object); in the 26.10.0 binary used for this page, the access printed an ExperimentalWarning and evaluated to undefined, so typeof localStorage is undefined and localStorage.setItem() fails with a TypeError. Pass the flag, or guard the access, rather than depending on either failure mode. --no-experimental-webstorage removes both globals. On Node.js 24 and 22 the flag --experimental-webstorage is required, otherwise sessionStorage is not defined at all. When the quota is exceeded the call throws a QuotaExceededError (a DOMException subclass added to the globals in 26.0.0).
sessionStorage.setItem('cart', '3');
console.log(sessionStorage.getItem('cart'), sessionStorage.length); // 3 1
sessionStorage.setItem('n', 42);
console.log(typeof sessionStorage.getItem('n')); // string: values are stringified
sessionStorage.clear();
localStorage.setItem('theme', 'dark');
console.log(localStorage.getItem('theme'), localStorage.length); // dark 1
localStorage.removeItem('theme');
try {
localStorage.setItem('big', 'x'.repeat(11 * 1024 * 1024)); // over the 10 MB quota
} catch (e) {
console.log(e.name, e instanceof DOMException);
}
3 1
string
dark 1
QuotaExceededError true
Source: Node.js API: Global objects (localStorage, sessionStorage) and Node.js API: Command-line options
The example ran as written on Node.js 26 with the flag; on Node.js 24 and 22 it needs --experimental-webstorage as well and prints an ExperimentalWarning. For anything that has to survive restarts and be safe under concurrent writes, use node:sqlite (SQLite built in) or a real database instead.
Temporal
Temporal is the modern replacement for the problem-ridden Date object: immutable types (Temporal.Instant, PlainDate, PlainTime, ZonedDateTime, Duration), explicit time zones and calendars, and exact arithmetic. It is a JavaScript language feature (an ECMAScript proposal implemented in V8), not a Node.js API, so it does not appear in the Node.js globals page and has no Node.js stability index; its availability follows the V8 version. In the binaries used for this page typeof Temporal is object on Node.js 26 (V8 14.6) and undefined on Node.js 24 (V8 13.6) and 22 (V8 12.4). The V8 flag --harmony-temporal also exposes it on those older lines, and --no-harmony-temporal hides it on 26, but V8 flags are not part of the Node.js compatibility promise; use a polyfill or keep using Date for code that must run on Node.js 24 and 22. Node.js itself began to use it in Node.js 26.2.0, where fs.Stats objects gained Temporal.Instant support. Node.js reports ERR_NO_TEMPORAL when an API needs Temporal but the binary was built without it or it is disabled (for example with --no-harmony-temporal).
console.log(typeof Temporal); // object (undefined on 24 and 22)
const lts = Temporal.PlainDate.from('2026-10-28');
console.log(lts.add({ weeks: 2 }).toString(), lts.dayOfWeek); // 2026-11-11 3
const instant = Temporal.Instant.from('2026-10-28T12:00:00Z');
console.log(instant.toZonedDateTimeISO('Europe/Madrid').toString()); // 2026-10-28T13:00:00+01:00[Europe/Madrid]
console.log(Temporal.Duration.from({ hours: 90 }).round({ largestUnit: 'day' }).toString()); // P3DT18H
console.log(Temporal.Now.instant() instanceof Temporal.Instant);
object
2026-11-11 3
2026-10-28T13:00:00+01:00[Europe/Madrid]
P3DT18H
true
Node.js has no dedicated Temporal page in its API documentation (the API docs mention it only for fs.Stats and the ERR_NO_TEMPORAL error), so the
official Node.js sources are the release notes: Node.js 26.0.0 enabled the Temporal API by default, and Node.js 26.2.0 added Temporal.Instant support to fs.Stats.
The API itself is documented by MDN.
Source: Node.js 26.0.0 release notes (Temporal API enabled by default), Node.js 26.2.0 release notes (Temporal.Instant in fs.Stats), Node.js API: Errors (ERR_NO_TEMPORAL), MDN: Temporal
The JavaScript date and time model is explained in Dates, errors and JSON.
Legacy globals
Stability 3 (Legacy) means a feature is still covered by semantic versioning and is unlikely to be removed, but is no longer actively maintained and has a better alternative. Three globals are in that category:
| Global | Replacement | Why |
|---|---|---|
|
|
|
|
|
They come from browser history, work on "binary strings" and throw |
|
|
Legacy since Node.js 22.7 and 20.18; its queue runs before promise callbacks and can starve the event loop. |
import { Buffer } from 'node:buffer';
const encoded = Buffer.from('Dune', 'utf8').toString('base64');
console.log(encoded, Buffer.from(encoded, 'base64').toString('utf8')); // RHVuZQ== Dune
console.log(btoa('Dune'), atob('RHVuZQ==')); // RHVuZQ== Dune
try { btoa('☕'); } catch (e) { console.log(e.name); } // InvalidCharacterError
console.log(Buffer.from('☕').toString('base64')); // 4piV
console.log(Buffer.from('☕').toString('base64url')); // 4piV (URL-safe alphabet)
console.log(globalThis === global);
RHVuZQ== Dune
RHVuZQ== Dune
InvalidCharacterError
4piV
4piV
true
The documentation offers an automated migration for atob and btoa (npx codemod@latest @nodejs/buffer-atob-btoa). Legacy is not Deprecated: none of these will stop working, but new code should use the replacement.
Other globals worth knowing
The API page lists many more classes. A few have special status on Node.js 26 and are worth a summary rather than an example:
| Global | Stability | Notes |
|---|---|---|
|
Stable |
Browser-compatible client; no longer experimental since Node.js 22.4. Disable with |
|
1 - Experimental |
Needs |
|
1 - Experimental |
Added in Node.js 26.9.0 and needs |
|
Stable |
|
|
Stable |
Web Crypto; only present when Node.js is built with |
|
Stable |
Standard error and event types; |
The stability index
Every section of the Node.js API documentation starts with a stability level, and this section of the documentation uses them in the same way. They are defined once, on the "About this documentation" page, and summarised here.
| Level | Name | Meaning |
|---|---|---|
0 |
Deprecated |
May emit warnings; backward compatibility is not guaranteed. Deprecations are numbered DEPnnnn and fall into documentation-only, application (a warning only for code outside |
1 |
Experimental |
Not subject to semantic versioning: non-backward-compatible changes or removal may occur in any release, and it is not recommended for production. Often gated by a flag and may print a warning. |
1.0 |
Early development |
Unfinished and subject to substantial change. |
1.1 |
Active development |
Nearing minimum viability ( |
1.2 |
Release candidate |
Hopefully ready to become stable; no further breaking changes are anticipated (Web Storage, |
2 |
Stable |
Compatibility with the npm ecosystem is a high priority. |
3 |
Legacy |
Still covered by semantic versioning, unlikely to be removed, but no longer maintained and with better alternatives ( |
Three habits follow from the table. First, read the level on the exact page for the API you use, because a whole module can be Stable while one method is Experimental (the global objects page itself is Stable while URLPattern is not). Second, when you write a library, remember that your users may not know you rely on an Experimental feature; flags and warnings exist to protect them. Third, Experimental features leave that state either by graduating or by being removed without a deprecation cycle (for example --experimental-transform-types was removed in Node.js 26.0.0), so keep Experimental usage behind a thin wrapper. The release lines and their support dates are in Release lines & LTS.
What changed recently
| Version | Change |
|---|---|
22.1.0 |
|
22.4.0 |
Web Storage added behind |
22.7.0 and 20.18.0 |
|
22.15.0 and 23.11.0 |
Web Streams classes, |
24.0.0 |
|
24.5.0 |
|
24.7.0 and 22.20.0 |
|
25.0.0 |
Web Storage enabled by default (Release candidate); |
26.0.0 |
Per the API history, |
26.2.0 |
|
26.9.0 |
Experimental |
Where the books are outdated
The two O’Reilly books used as background for this section, Efficient Node.js (Samer Buna, 2025) and Node.js Projects (Jonathan Wexler, 2025), were written before Node.js 25 and 26 shipped, so they cannot describe the state of this page. Anything they say about which APIs are global should be re-checked against the table above: localStorage and sessionStorage are on by default only from Node.js 25 and need --localstorage-file to work; Temporal is available without flags only from Node.js 26; URLPattern is a global from Node.js 24 and still Experimental; navigator.locks and the Web Worker global are newer than Node.js 22. Equally, advice to install a third-party package for HTTP requests, UUIDs or deep cloning deserves a second look now that fetch, crypto.randomUUID() and structuredClone() are built in, and examples that call process.nextTick(), global, atob or btoa should use the replacements in the Legacy table. The stability labels (Experimental, Release candidate, Legacy) are likewise a moving target: re-read the page of the API on the Node.js version you actually run.