Running Code

Learn how to run files, evaluate code, preload modules, and handle program output.

This page shows the ways to hand nstd a program: a file, inline code, or a module loaded ahead of time. It also covers what require() and import() can reach, how the filesystem shows up, and what a program's exit code and uncaught errors look like.

#Running a script

Pass a file. It runs as the program's entry point, and anything after it becomes the program's own arguments:

nstd script.js one two

nstd run script.js does the same thing, spelled out. Node decides whether an entry is CommonJS or an ES module by its extension and its nearest package.json, and nstd makes the same call, so a .mjs file runs as a module and a .js or .cjs file runs as CommonJS (unless package.json says "type": "module").

Inside the program, process.argv looks like Node's: the path to the nstd binary, the resolved path to the script, then one and two.

#Evaluating code directly

-e (or --eval) runs a string instead of a file:

nstd -e 'console.log(require("os").hostname())'

There is no entry file in this form, so no script path is inserted into process.argv: process.argv[1] is whatever argument came right after the eval string, matching node -e.

#Loading a module first

-r, or --require, loads a CommonJS module before your program runs. Give it more than once to load several, in order:

nstd -r ./setup.cjs script.js
nstd -r ./a.cjs -r ./b.cjs script.js

A preload shares the realm with the rest of the program - if it patches a global or changes a module's state, your entry point sees the change. If a preload throws and nothing catches it, the program stops before the entry point ever runs.

NODE_OPTIONS carries the same option, which matters for a child process:

NODE_OPTIONS='--require ./setup.cjs' nstd script.js

A wrapper script only arranges the one process it starts, but a child spawned with child_process.fork() or as process.execPath starts fresh and carries nothing across except its environment - so NODE_OPTIONS is the only way a preload reaches it too. See CLI Options for the rest of what NODE_OPTIONS can carry.

#Seeing what this binary can serve

--print-realm starts up the JavaScript environment - the "realm" - and reports on it instead of running a program:

nstd --print-realm
nstd 0.0.1 (quickjs-ng ..., zig ...)
node v26.7.0 on linux/x64

67 builtin(s) registered, 0 unavailable
  + assert
  + buffer
  + fs
  ...

Every node: module this build could evaluate is listed with a +; anything that failed is listed with a - and the reason. This is the fastest way to check whether a given build of nstd has what your program needs before you run it.

#What require() and import() do here

require() reaches three kinds of things: the node: builtins compiled into the binary, your own files on disk, and packages in node_modules, resolved the same way Node resolves them - "exports", "imports", and the plain walk up the directory tree when a package has neither.

import() and .mjs files work too, including relative specifiers, packages with no "exports" field, node: builtins, CommonJS and JSON dependencies, import attributes, cycles and top-level await. require() of an ES module works as well, and a CommonJS file can import() another module dynamically and have the specifier resolve relative to itself, the way Node resolves it. Named imports out of a .cjs file (import { readFile } from './some.cjs') work too, using the same CommonJS parser Node's own binding uses.

See Node.js Compatibility for what module loading does not cover yet.

#The filesystem

fs, require(), and everything that resolves a path talk to the real filesystem on the machine nstd runs on - there is no sandbox and no virtual root. A relative path resolves against the process's real working directory, and fs.watch()/fs.watchFile() both work. Treat a script run with nstd exactly as you would treat one run with node: it can read, write and delete anything the user running it can.

#Exit codes and uncaught errors

A program that runs to completion without calling process.exit() leaves with process.exitCode, which defaults to 0 and which your code can set directly, exactly as in Node.

An exception nothing catches is fatal: the program prints it and exits 1, unless a process.on('uncaughtException', ...) listener (or process.setUncaughtExceptionCaptureCallback()) claims it, in which case the program keeps running. An unhandled promise rejection follows the same path - process.on('unhandledRejection', ...) sees it first, and if nothing claims it there either, it is fatal the same way an uncaught throw is.

A few situations exit with a different code, the same way they do under node:

exit codewhen
0the program finished, or --help/--version/--print-realm ran
1an uncaught exception or unhandled rejection, or process.exit(1)
9an option on the command line or in NODE_OPTIONS was refused
70the realm itself failed to start (a bug, not a program error)

A script file that does not exist is reported the same way: a message on stderr and exit 1, before any of your code runs.