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()andmachine()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()andarch()report whatever the host declared, not the machine that built the library: both come fromprocess.platformandprocess.arch, andnstdlib/globalsis where a host sets them - defaulting to"browser"and"x64"if it does not.tmpdir()readsTMPDIR,TMPorTEMPfromprocess.env, falling back to/tmp- an environment read, not a machine read.availableParallelism()answers the host's ownnavigator.hardwareConcurrencywhere one exists, and throws where it does not, rather than guessing1.
#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 hasinternalBinding('quic'), so there is nothing here to build on. Thequicmodules fail by design, andinternal/quic/quicis the one internal module this project deliberately does not publish at all, because it throws without--experimental-quicand 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().