
# 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](/api).
The [Host](/nstdlib/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](/nstdlib/host). 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](/nstdlib/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](/guide) 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](/nstdlib/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`](/nstdlib/host) 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()`.
