Node.js Compatibility

See which Node.js features nstd supports, what is missing, and how compatibility is tested.

nstd runs Node programs without Node installed. This page explains how closely it follows real Node, what works, what doesn't, and what happens when your program uses something nstd can't do.

#Which Node version?

nstd's standard library is built from Node's own source code, pinned to v26.10.0. The JavaScript behind fs, path, stream, url and the rest isn't a reimplementation. It's Node's own code, adapted to run without Node underneath. Where a module's logic matches Node v26.10.0, that's because it is the same code.

That doesn't mean nstd behaves like Node v26.10.0 in every way. Whenever Node's standard library needs the operating system (files, sockets, encryption, processes), it calls into C++. nstd answers those calls with its own host, written in Zig, using different libraries: QuickJS-NG instead of V8, and Mbed TLS instead of OpenSSL. Where the host provides a feature, Node's JavaScript runs unchanged on top of it. Where it doesn't, the module still loads, but the specific call that needs the missing piece throws an error. See What doesn't work below.

#What works

A large part of the standard library behaves like the real thing, including file and network access:

  • Core utilities. path, util, events, stream, buffer, string_decoder, querystring, punycode, assert, url and the URL/URLSearchParams globals all run Node's own code and are checked against real Node's output.
  • The real filesystem. fs and fs/promises work on the actual disk. fs.watch() (including recursive: true) and fs.watchFile() both work.
  • require() and import(). Both module loaders are Node's own, including node_modules lookup, "exports" in package.json, JSON files and .mjs files.
  • Randomness, WebCrypto and hashing. crypto.randomBytes(), randomUUID(), randomInt(), timingSafeEqual() and all of crypto.subtle work. createHash(), createHmac() and crypto.hash() support fifteen algorithms, computed by Mbed TLS or, for sha512-224, sha512-256, shake128 and shake256, in JavaScript.
  • DNS. dns.lookup() and the dns.resolve*() functions return real answers.
  • Networking and TLS. net.connect(), http.request(), https.request() and fetch() open real connections, including TLS. Servers work too: net, http and https can listen() on TCP and Unix domain sockets.
  • Child processes. spawn, exec, execFile, fork and cluster all start real processes.
  • Worker threads. new Worker() starts a real thread with its own JavaScript engine.
  • Compression. gzip, deflate, deflateRaw and unzip work, both as one-shot functions and as streams, and so does zlib.crc32(). brotliDecompress works too, so HTTP responses compressed with gzip or br are decoded like in any other client.
  • Machine information. os.hostname(), platform(), arch(), homedir(), tmpdir(), totalmem(), freemem(), uptime(), userInfo(), networkInterfaces() and, on Linux, cpus() all report the real machine.

#What doesn't work

Some features aren't built yet, or have no equivalent without the libraries Node uses:

  • Crypto that needs OpenSSL. Ciphers, key objects, signatures, Diffie-Hellman and X.509 certificates rely on OpenSSL, which nstd doesn't include. crypto.getCiphers() returns []. (Hashing isn't affected; see above.)
  • TLS servers. tls.connect() and https.request() work as clients, but tls.createServer() and https.createServer() throw. For the same reason, TLS session resumption isn't available.
  • Brotli compression and Zstandard. zlib.brotliCompress(), zlib.zstdCompress() and zlib.zstdDecompress() throw. Everything DEFLATE based works, and so does Brotli decompression, which is what an HTTP client needs.
  • Sending a handle to a child process. subprocess.send(message, handle) throws. Everything else about child processes works.
  • os.loadavg(). Not available.
  • Non-UTF-8 filenames. fs.watch() and fs.readdir() throw if you ask for buffer, hex, latin1 or base64 filename encodings, instead of quietly returning strings.
  • The exact error in an unhandled rejection. An unhandled rejection stops the program and process.on("unhandledRejection", ...) fires, just like in Node. But when the rejection reason is an Error, nstd reports it wrapped in an UnhandledPromiseRejection object instead of as the original error. This happens because QuickJS-NG stores an error's stack differently from V8. The exit code and the message are correct; only the reported object differs.

For the complete picture, module by module, see the API support pages.

#When something isn't supported

When you call something nstd can't do, it never returns undefined or fails silently. It throws an error that names exactly what's missing, with error.code set to ERR_NSTDLIB_NO_BINDING. For example, crypto.createCipheriv() throws:

Error: [nstdlib] internalBinding('crypto').CipherBase() is not implemented:
this runtime has no such binding, and no host implementation is installed on
`globalThis.node_binding_crypto_CipherBase`.

TLS server features throw the same way, with the code ERR_NSTDLIB_NO_OPENSSL, so you can tell the two cases apart.

This is deliberate. Earlier versions returned undefined instead, and that caused subtle bugs. undefined looks like a real answer, so code kept running with the wrong value. timingSafeEqual() treated every comparison as unequal, and a dns.lookup() call never called back, so the program hung without an error.

A thrown error shows up immediately, with a stack trace pointing at the call that failed. If your program doesn't behave as expected, look for one of these errors first. A missing feature that announces itself is much easier to work around than one that quietly gives the wrong answer.

#How compatibility is tested

The claims on this page are checked in two independent ways, both based on Node itself rather than on a description of Node:

  • Node's own test suite. Thousands of files from Node's test/parallel, the most complete description of how each builtin should behave, run directly on nstd with no Node underneath.
  • Side-by-side tests. The same program runs under real Node and under nstd, and the two outputs are compared byte for byte. These cover URL parsing, networking, HTTP parsing, TLS, child processes, the filesystem and more.

The results change as the project moves, so this page doesn't repeat them. The API support pages show the current status of every module: which ones match Node's behavior, which ones only have the right shape and throw when called, and which ones haven't been tested yet.

You can also check one number yourself: nstd --print-realm shows how many builtin modules loaded at startup, and lists any that didn't.