Limits

Understand which Node.js features nstdlib cannot currently provide and how unsupported calls fail.

This page lists what does not work, what "works" is qualified by, and why. For the current state of every module, see the API reference. The Host page explains how a runtime can provide some missing features.

#How a missing capability fails

When nstdlib cannot do something, it throws a named error instead of quietly returning undefined. A wrong answer delivered silently is worse than no answer, so every unbacked capability fails loudly, by name, at the point you call it - not sometime later when a caller trusts the undefined it got back:

Error: [nstdlib] internalBinding('fs').open() is not implemented: this runtime
has no such binding, and no host implementation is installed on
`globalThis.node_binding_fs_open`. To get a filesystem without one, mount a
node:vfs provider: (await import("nstdlib/vfs")).default.create().mount("/")
- in memory, or your own VirtualProvider.

The error's code is ERR_NSTDLIB_NO_BINDING, and the message names both the capability and the global a host would install to supply it. An fs error goes further, as above: it also points at the cheaper fix, since most of the time what you actually want is not a host at all but a memory-mounted filesystem. A few related codes cover capabilities that fail for a different reason and say so: ERR_NSTDLIB_NO_MODULE_COMPILER (below), ERR_NSTDLIB_NO_CJS_LEXER (named exports of a CommonJS module) and ERR_NSTDLIB_NO_ASYNC_CONTEXT_FRAME.

#Crypto and TLS: no OpenSSL

crypto.getCiphers() honestly answers []. Hashing, HMAC, ciphers, keys, signatures, Diffie-Hellman and X.509 all need OpenSSL primitives with no portable equivalent, so createHash(), createHmac() and crypto.hash() throw by name. This is the one gap the project does not expect to close from inside the library itself.

Randomness is the exception and really works everywhere: randomBytes(), randomUUID() and crypto.subtle run on the host's own WebCrypto. tls has the same shape as crypto - no SecureContext without a host behind it, so tls.connect() and anything over HTTPS that needs to verify a certificate throws. tls.createServer() throws everywhere; nothing here can accept a TLS connection, only make one, and only where a host supplies the engine.

#Sockets: net, http, https, http2, dgram

There are no sockets here without a host. net and http are both backed by one binding, and nstdlib implements almost all of the protocol around it itself - connecting, listening, the accept loop, backlog handling - over a single function a host provides: a non-blocking read/write/watch bridge for one file descriptor. Install that bridge and both directions work: net.connect()/http.request() as a client, and net.createServer().listen() as a server. See Host for details about the required host function. Without a host, new net.Socket() still constructs, but connect() throws, and so does listen().

https rides the same client bridge plus a TLS layer, so https.request() works too wherever a host also provides TLS support - but only as a client. https.createServer() and tls.createServer() throw everywhere, in every runtime, because only the client half of TLS is implemented; every session this library opens is one it started, never one it accepted. http2 and dgram are different again: nothing here backs their bindings at all, host or no host, so both throw regardless.

The HTTP message parser itself is unaffected by any of this - it is nstdlib's own code, diffed against Node's own llhttp, not a host question.

#Child processes, threads and compression

child_process follows the same pattern as sockets. Everything above the binding is nstdlib's own code, behind one function each a host installs (node_binding_process_wrap_dispatch for spawn(), and node_binding_spawn_sync_dispatch for spawnSync()), and with no host both throw. Where a host does supply them, spawn, exec, execFile, fork and cluster all work - nstd is the existence proof. Passing a handle over IPC is the one part no host here implements.

zlib is the same shape: no compression is built in, so every stream and one-shot function throws, and a host implementation behind node_binding_zlib_Zlib lifts it. Nothing supplies one today, including nstd, which is why a gzipped HTTP response is the gap you are most likely to meet first.

worker_threads is the exception. It is not one bridge away - threads need a whole second realm with its own message port, and nothing here provides one, so new Worker() throws everywhere.

#The filesystem, without a mount

fs has no real filesystem behind it. A path that is not inside a mounted node:vfs provider has nothing there, so fs.readFileSync("/etc/hosts") throws. Mount a memory filesystem and the whole fs API works against it - see Host. Two edges are worth knowing even with a mount: fs/promises.glob degrades to a stub and throws by name, because the matcher it needs lives outside the part of Node this project's build reaches; and existsSync() on a path outside every mount answers undefined rather than false.

#os: some real answers, mostly refusals

Most of os reads the actual machine, which nstdlib does not have: homedir(), uptime(), networkInterfaces(), cpus(), loadavg(), freemem(), totalmem() and userInfo() all throw ERR_NSTDLIB_NO_BINDING. networkInterfaces() throwing is a deliberate choice rather than an oversight: an empty list is easy to mistake for "no IPv6 here" and would fail silently, where the throw at least says a capability is missing.

A few names do answer, and none of them by reading a machine:

  • hostname() answers "localhost", the name RFC 6761 reserves for "the host I am running on."
  • type(), version(), release() and machine() answer empty strings rather than throwing - Node's own code reads all four out of one call at module scope, so a throw there would take the whole module down at import.
  • platform() and arch() report whatever the host declared, not the machine that built the library: both come from process.platform and process.arch, and nstdlib/globals is where a host sets them - defaulting to "browser" and "x64" if it does not.
  • tmpdir() reads TMPDIR, TMP or TEMP from process.env, falling back to /tmp - an environment read, not a machine read.
  • availableParallelism() answers the host's own navigator.hardwareConcurrency where one exists, and throws where it does not, rather than guessing 1.

#require() needs dynamic code evaluation

Loading a CommonJS module compiles it with new Function(source). Anywhere that is forbidden - a page under a Content-Security-Policy without unsafe-eval, a Cloudflare Worker, an isolate started with --disallow-code-generation-from-strings - gets no require() at all, not a degraded one. Everything else in the library runs in that environment. import() of a CommonJS module needs the same capability, for the same reason: it goes through the same compiler underneath.

#ES module source text cannot be compiled here

import() of a CommonJS module, a JSON file or a registered builtin works, resolver and all. Compiling the source text of an actual .mjs file from a mount does not, and this is a limit of the language rather than a gap this project can close with more code: no standard JavaScript API exposes a module compiler whose resolver the caller controls. new Function cannot express an import statement, and import() itself compiles with the host's resolver against the host's own filesystem - the very thing being replaced.

Error: [nstdlib] cannot compile the ES module at file:///app/mod.mjs: the
source-text form of ModuleWrap hands source to
v8::ScriptCompiler::CompileModule(), and this runtime has no module compiler.

The error code is ERR_NSTDLIB_NO_MODULE_COMPILER. Two things actually work around it, both outside this library: a host whose JavaScript engine has its own compiler can install one - the standalone nstd runtime does, over QuickJS-NG - or you can hand the loading to a host that already compiles modules for you: Node's own module.registerHooks(), or a service worker serving your mount's files to the browser's own import().

#A short list of things that never work

  • quic - no official Node build has internalBinding('quic'), so there is nothing here to build on. The quic modules fail by design, and internal/quic/quic is the one internal module this project deliberately does not publish at all, because it throws without --experimental-quic and an entry that cannot be imported is worse than an absent one.
  • ffi - needs a real dynamic linker.
  • internal/webstorage - needs a storage file on disk.
  • internal/main/* - Node's own process entry points. They exist, but importing one asks a Node process to boot, which nstdlib does not do.

#Where a shim changes behaviour instead of throwing

Some gaps are filled with a reasonable answer rather than a throw, and every place that happens is documented at the top of the file responsible for it (under shim/stub/binding/ in the source, if you want to check one). The one worth knowing about even in application code: AsyncLocalStorage does not propagate across an await, because the V8 hook it needs is not reachable from JavaScript. The synchronous parts of run() and enterWith() work, and so does anything wrapped with AsyncResource.bind() or AsyncLocalStorage.snapshot().