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 twonstd 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.jsA 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.jsA 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-realmnstd 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 code | when |
|---|---|
0 | the program finished, or --help/--version/--print-realm ran |
1 | an uncaught exception or unhandled rejection, or process.exit(1) |
9 | an option on the command line or in NODE_OPTIONS was refused |
70 | the 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.