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

FeatureOut of the boxWith 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:

CodeMeaning
ERR_NSTDLIB_NO_BINDINGThe feature needs something your runtime hasn't provided.
ERR_NSTDLIB_NO_MODULE_COMPILERAn .mjs file needs a module compiler. See below.
ERR_NSTDLIB_NO_CJS_LEXERNamed imports from a CommonJS module need a CommonJS lexer.
ERR_NSTDLIB_NO_ASYNC_CONTEXT_FRAMECode 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.glob doesn'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 returning false. nstdlib can't see anything there, so "doesn't exist" would be a guess.
  • fs.watch() works, but may poll. It uses your provider's watch() if it has one, and otherwise checks for changes every 100 ms. MemoryProvider polls 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:

FunctionReturns
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 allows new Function(), like require() 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 own import().

#Things that never work

  • quic. No official Node build includes QUIC support, so there's nothing to build on. The internal/quic/quic module 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().