Limitations
Understand which Node.js features nstdlib cannot currently provide and how unsupported calls fail.
This page lists what doesn't work in nstdlib, what only works with help from your runtime, and why. For the status of each individual module, see the API reference. For how to provide missing features, see Host Integration.
#At a glance
| Feature | Out of the box | With a host |
|---|---|---|
| Pure JavaScript modules | ✅ works | ✅ works |
| Randomness, WebCrypto, hashing | ✅ works | ✅ works |
| DNS | ✅ over HTTPS | ✅ your own resolver |
fs | ❌ throws | ✅ installFileSystem() |
net, http | ❌ throws | ✅ installNetwork() |
https, tls as a client | ❌ throws | ✅ with a host TLS engine |
child_process (async) | ❌ throws | ✅ installProcesses() |
worker_threads | ❌ throws | ✅ installWorkers() |
zlib | ❌ throws | ✅ with a host implementation |
require() | ✅ if eval is allowed | ✅ if eval is allowed |
.mjs source files | ❌ throws | ✅ nstdlib/esm |
| Ciphers, keys, signatures | ❌ throws | ❌ throws |
| TLS and HTTPS servers | ❌ throws | ❌ throws |
http2, dgram | ❌ throws | ❌ throws |
#How an unsupported call fails
When nstdlib can't do something, it throws an error that says so, right where
you made the call. It never quietly returns undefined, because a wrong answer
that looks right is much harder to debug than an error:
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, install a
node:vfs provider under the binding: (await import("nstdlib/globals"))
.installFileSystem(globalThis, new (await import("nstdlib/node/vfs"))
.MemoryProvider()) - in memory, or your own VirtualProvider.The error's code is ERR_NSTDLIB_NO_BINDING. The message names the missing
feature and the global a host could define to provide it. fs errors also
suggest the easier fix: most of the time you just want an in-memory
filesystem.
A few other codes cover different reasons:
| Code | Meaning |
|---|---|
ERR_NSTDLIB_NO_BINDING | The feature needs something your runtime hasn't provided. |
ERR_NSTDLIB_NO_MODULE_COMPILER | An .mjs file needs a module compiler. See below. |
ERR_NSTDLIB_NO_CJS_LEXER | Named imports from a CommonJS module need a CommonJS lexer. |
ERR_NSTDLIB_NO_ASYNC_CONTEXT_FRAME | Code tried to use V8's async context frames, which no runtime here has. You shouldn't normally see this. |
#Crypto and TLS
nstdlib doesn't include OpenSSL, so anything that depends on it throws:
ciphers, key objects, signatures, Diffie-Hellman, X.509 certificates and
scrypt. crypto.getCiphers() returns [].
Hashing does work everywhere. Hashing is just arithmetic, so nstdlib
computes it in JavaScript. MD5, SHA-1, SHA-224/256/384/512, SHA-512/224,
SHA-512/256, SHA3-224/256/384/512 and SHAKE128/256 all work with
createHash(), createHmac(), crypto.hash(), pbkdf2() and hkdf(), with
no host needed. crypto.getHashes() lists exactly these. Other algorithms,
such as RIPEMD-160, BLAKE2 or SM3, throw rather than being approximated. If a
host provides a native implementation (through
node_binding_crypto_digest_dispatch, as nstd does), nstdlib uses that
instead.
Randomness works everywhere too. randomBytes(), randomUUID() and
crypto.subtle use your runtime's built-in WebCrypto.
tls needs a TLS engine from the host. Without one, tls.connect() and any
HTTPS request that verifies a certificate throw. TLS servers never work.
tls.createServer() and https.createServer() throw in every runtime,
because only the client side of TLS is implemented.
#Networking
There are no sockets without a host. nstdlib implements almost everything in
net and http itself, including connecting, listening and accepting
connections. All it needs from a host is one function that reads and writes a
socket. Provide that, and both clients (net.connect(), http.request()) and
servers (net.createServer().listen()) work. Without it, you can still create
a net.Socket, but connect() and listen() throw.
installNetwork() provides that function anywhere,
including in a browser, but it's an in-memory network. Servers and clients in
the same page can talk to each other, and connecting to anything else fails
with ECONNREFUSED. It doesn't give a page a real socket, and nothing can. To
reach the outside world, you pass it a provider, such as a relay or a data
channel.
https uses the same connection plus a TLS layer, so https.request() works
wherever a host also provides TLS, as a client only.
http2 and dgram (UDP) have no implementation at all, with or without a
host, so they always throw.
The HTTP message parser is not affected by any of this. It's part of nstdlib itself and is tested against Node's own parser.
#Child processes
child_process works like networking: nstdlib implements everything on top of
one function per API, which a host provides (node_binding_process_wrap_dispatch
for spawn() and node_binding_spawn_sync_dispatch for spawnSync()).
Without them, both throw. With them, spawn, exec, execFile, fork and
cluster all work, as nstd shows.
In a page or other embedding,
installProcesses() provides the asynchronous
half on top of your own CommandHost. It doesn't support the synchronous
functions like spawnSync(), since most embedders can't give a synchronous
answer.
Passing a handle (such as a socket) to a child process over IPC isn't supported anywhere.
#Worker threads
A worker needs a second, separate JavaScript environment running on another
thread, which JavaScript can't create by itself. Without a host,
new Worker() throws. A host that can run one installs it with
installWorkers() (see Host Integration).
nstd does this, so worker threads work there.
#Compression
zlib also needs a host. Without one, every compression function and stream
throws. A host provides it through node_binding_zlib_Zlib and
node_binding_zlib_BrotliDecoder. nstd supplies
both, so it can read gzip and br HTTP responses. No host here supports
Brotli compression or Zstandard in either direction; those would each add
another library to the build.
#The filesystem
Without an installed filesystem, fs has nothing to read, so a call like
fs.readFileSync("/etc/hosts") throws. Install one and the whole fs API
works against it (see Host Integration). Even then, a few
things are worth knowing:
fs/promises.globdoesn't work. It throws, because the pattern matcher it needs isn't part of the code nstdlib is built from.existsSync()outside every mount throws instead of returningfalse. nstdlib can't see anything there, so "doesn't exist" would be a guess.fs.watch()works, but may poll. It uses your provider'swatch()if it has one, and otherwise checks for changes every 100 ms.MemoryProviderpolls too, so you get at most one"change"event per interval, and none for a write that leaves the file size and modification time unchanged.fs.watchFile()polls at the interval you give it.fs.statfs()reports a 4096-byte block size and no free space, because a provider has no disk blocks to count.- Absolute symlink targets resolve inside the provider. Providers see
paths relative to their mount point. At
/that makes no difference; under a mount like/mem, use relative symlink targets.
#os
Most of os describes the actual machine, which nstdlib can't see.
homedir(), uptime(), networkInterfaces(), cpus(), loadavg(),
freemem(), totalmem() and userInfo() all throw ERR_NSTDLIB_NO_BINDING.
(networkInterfaces() throws on purpose: an empty list could be mistaken for
"no network here".)
A few functions do return a value:
| Function | Returns |
|---|---|
hostname() | "localhost" |
type(), version(), release(), machine() | "", so that importing os doesn't fail |
platform(), arch() | process.platform and process.arch, which default to "browser" and "x64" and can be set with installGlobals() |
tmpdir() | TMPDIR, TMP or TEMP from process.env, or /tmp |
availableParallelism() | navigator.hardwareConcurrency, or throws if there isn't one |
#require() needs eval
require() compiles CommonJS modules with new Function(). Where that's
forbidden (a page with a Content-Security-Policy that doesn't allow
unsafe-eval, a Cloudflare Worker, or a runtime started with
--disallow-code-generation-from-strings), require() isn't available at
all. import() of a CommonJS module has the same limit. Everything else in
nstdlib still works in those environments.
#ES module source files
import() of a builtin, a CommonJS module or a JSON file works out of the box.
Compiling an actual .mjs file does not, because JavaScript has no standard
way to compile a module while controlling how its imports resolve. Without a
compiler, you get:
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. There are three ways
around it:
- Install the compiler from
nstdlib/esm. It needs a runtime that allowsnew Function(), likerequire()does. - Use a JavaScript engine that has its own module compiler. nstd does this with QuickJS-NG.
- Let something that already compiles modules do the loading, such as Node's
module.registerHooks(), or a service worker that serves your files to the browser's ownimport().
#Things that never work
quic. No official Node build includes QUIC support, so there's nothing to build on. Theinternal/quic/quicmodule isn't published at all, because it can't even be imported without--experimental-quic.ffi. It needs a real dynamic linker.internal/webstorage. It needs a storage file on disk.internal/main/*. These are Node's process entry points. Importing one asks a Node process to start up, which nstdlib doesn't do.
#Where behavior differs instead of throwing
In a few places, nstdlib fills a gap with a reasonable substitute instead of
throwing. Each one is documented at the top of the file responsible for it,
under src/node/stub/binding/ in the source.
The one most likely to affect your code: AsyncLocalStorage doesn't carry
its value across an await, because the V8 feature it relies on isn't
available from JavaScript. The synchronous parts of run() and enterWith()
work, and so does anything wrapped with AsyncResource.bind() or
AsyncLocalStorage.snapshot().