API reference

Process & CLI

33/33Coverage21/33Strict (zero caveats)

What a CLI tool or containerized service needs day-to-day: argv, environment, exit codes, stdin, signals, timing, and child processes. The recurring divergence: every event/handler API here takes one listener slot per event, an arrow/function-literal only, firing synchronously from the event loop's dispatch rather than through a microtask queue. child_process (spawn/exec/fork/spawnSync) lives here too — it is what a process orchestrator actually reaches for.

Exact Works, with caveats Not supported

process.argv

Exact
process.argv: string[]

The command-line arguments, in Node's shape: argv[0] and argv[1] are the executable's absolute path (a compiled program is its own script) and the arguments follow, so process.argv.slice(2) is what the user typed. An out-of-range read is undefined.

Returns

string[] — The argument vector.

const args: string[] = process.argv
for (const a of args) console.log(a)
See it in a full example →

process.env

Exact
process.env.KEY: string | undefined; process.env["KEY"]

Environment variables, via both dot and bracket notation. A missing variable is undefined (typeof included), so ??, === undefined and narrowing behave as in Node.

Returns

string | undefined — The value, or undefined if unset.

const path: string = process.env.PATH ?? "no PATH set"
See it in a full example →

process.exit(code)

Exact
process.exit(code?: number): never

Exits the process: the code (validated like exitCode, or process.exitCode when omitted) becomes the exit code, 'exit' fires once, then C exit(); code after it is unreachable.

Returns

never — Does not return.

fs.readFileSync(0)

Exact
readFileSync(0, 'utf8'): string

Reads all of stdin (file descriptor 0) to its end, blocking — the Node idiom for a CLI tool that takes piped input. A line-at-a-time read is the readline module.

Returns

string — Everything on stdin.

import { readFileSync } from 'fs'
const input = readFileSync(0, 'utf8')
console.log(input.split('\n')[0])

Synchronous file read/write

Exact
fs.readFileSync(path); fs.writeFileSync(path, data)

Config, data, and log I/O for a CLI program — the full sync/async/stream fs surface has its own reference page.

process.cwd()

Exact
process.cwd(): string

The current working directory, via POSIX getcwd(NULL, 0) (auto-sizing).

Returns

string — The current directory.

const startDir: string = process.cwd()
See it in a full example →

process.chdir(path)

Exact
process.chdir(path: string): void

Changes the current working directory via POSIX chdir(). A failure throws Node's error: ENOENT: no such file or directory, chdir '<cwd>' -> '<dir>', with errno, code, syscall, path and dest.

Returns

void — Nothing.

process.chdir("/tmp")
See it in a full example →

process.pid

Exact
process.pid: number

The current process ID, via POSIX getpid(). A property read, not a call.

Returns

number — The process ID.

process.platform

Exact
process.platform: string

A pure compile-time constant ("darwin"/"linux"/"win32"/…) baked in at codegen — no runtime code. Under the --target cross-compile flag it follows the target: codegen reads the target OS/arch through a target abstraction, so an OS-aligned (e.g. Linux→Linux) or macOS→Linux cross build bakes the target's platform, not the build host's.

Returns

string — The OS platform.

console.log(process.platform, process.arch)
See it in a full example →

process.arch

Exact
process.arch: string

A compile-time constant ("arm64"/"x64"/…) from the Go compiler's runtime.GOARCH, exactly like process.platform.

Returns

string — The CPU architecture.

process.execPath

Exact
process.execPath: string

The absolute, symlink-resolved path of the running executable — the compiled program is itself the binary (no separate runtime). Always absolute like Node's, not a raw argv[0].

Returns

string — The executable's absolute path.

process.emitWarning(warning, type?)

Works, with caveats
process.emitWarning(warning: string | Error, type?: string | { type?: string; code?: string; detail?: string }, code?: string): void

Emits the process 'warning' event on the next tick with an Error whose name is the type ("Warning" by default), with code and detail when given; the default listener prints (node:<pid>) [<code>] <name>: <message> and a <detail> line to stderr, as Node's does.

Returns

void — Nothing.

process.emitWarning("legacy call site", { type: "DeprecationWarning", code: "DEP001" })
See it in a full example →
⚠ Differences from standard
  • The one-shot (Use node --trace-warnings ...) hint Node prints is omitted: a compiled program has no --trace-warnings flag.

process.kill(pid, signal?)

Exact
process.kill(pid: number, signal?: number | string): true

Sends signal (SIGTERM by default) to pid via POSIX kill(). The signal may be a name ('SIGTERM', 'SIGINT', …) or a number; an unknown name throws ERR_UNKNOWN_SIGNAL. A failure throws Node's 'kill <code>' Error with code, errno and syscall; signal 0 is the standard existence check and delivers nothing. On Windows SIGTERM/SIGKILL/SIGINT/SIGQUIT terminate the target unconditionally (exit code 1), as in Node, and any other signal throws ENOSYS.

Returns

true — true, when the signal was sent.

process.stdout.write / stderr.write

Works, with caveats
process.stdout.write(s: string): void; process.stderr.write(s: string): void

process.stdout and process.stderr are Node's streams, made on first use by what the descriptor is: a tty.WriteStream on a terminal, a net.Socket on a pipe or socket, a synchronous Writable on a file. write takes a string or a Buffer, an encoding and a callback, and appends nothing — unlike console.log/.error's trailing newline. Writes to a terminal, a file or a POSIX pipe are synchronous, as in Node, so output stays ordered with console.log; pipe() into them works.

Returns

void — Nothing.

process.stdout.write(chunk.toUpperCase())
See it in a full example →
⚠ Differences from standard
  • Text-only (byte-string, not binary-safe past an embedded null — the same scope narrowing console.log has).
  • Beyond .write, .isTTY (a real isatty(fd) probe), and .columns/.rows (a live ioctl(TIOCGWINSZ) read), process.stdout/stderr expose no other stream surface (no 'drain').

process.stdout.columns / .rows

Exact
process.stdout.columns: number; process.stdout.rows: number

The terminal's width and height, on a tty.WriteStream: read at creation and refreshed on SIGWINCH, which emits 'resize' when the size changed. Off a TTY (piped/redirected) the stream has no columns/rows: a real undefined, as in Node; ?? 80 is the idiomatic fallback.

Returns

number | undefined — Columns or rows, or undefined when the fd is not a terminal.

console.log(process.stdout.columns + "x" + process.stdout.rows)
See it in a full example →
⚠ Differences from standard
  • Stored into a declared number binding off a TTY, the value reads 0, not undefined.

process.stdin.setRawMode / klain:tty

Exact
process.stdin.setRawMode(enabled: boolean): void

A tty.ReadStream method that puts the terminal into raw mode (libuv's UV_TTY_MODE_RAW): canonical mode, echo and signal generation are off, so keystrokes arrive one at a time and Ctrl-C comes through as the byte 3; isRaw reports the mode, and the original mode is restored at exit. When fd 0 is not a terminal, process.stdin is a net.Socket without setRawMode, as in Node — guard on process.stdin.isTTY.

Returns

void — Nothing.

import { readKey } from 'klain:tty'
process.stdin.setRawMode(true)
const k = readKey()
See it in a full example →
⚠ Differences from standard
  • A no-op when fd 0 is not a terminal (tcgetattr fails); setRawMode(false) restores the state captured on the first enable.
  • Auto-restore on a crash is the application's responsibility (same footgun as Node).
  • readByte()/readKey() are a bespoke, non-Node klain: surface — Node has no synchronous single-key read. Don't mix them with process.stdin.on('data') in the same run (that puts fd 0 in non-blocking mode).

klain:tty readByte / readKey

Works, with caveats
import { readByte, readKey } from 'klain:tty'; readByte(): number; readKey(): string

Synchronous single-keystroke reads off fd 0, for a blocking raw-input loop. readByte() returns one byte (or -1 at EOF); readKey() returns one read()'s whole burst as a string, so an escape sequence like an arrow key (ESC [ A) is a single key. A bespoke klain: primitive because Node has no synchronous single-key read — it uses process.stdin.on('data') events.

Returns

number | string — The byte (-1 at EOF) or the keystroke string.

import { readKey } from 'klain:tty'
const k = readKey()
See it in a full example →
⚠ Differences from standard
  • Non-Node surface under the explicit klain: specifier.
  • Blocking reads on fd 0; don't mix with process.stdin.on('data') in the same run (which puts fd 0 in non-blocking mode, so a synchronous read would see EOF-on-EAGAIN).
  • Pair with process.stdin.setRawMode(true) so keystrokes arrive un-echoed, one at a time.

Writing process.env

Exact
process.env.KEY = val; process.env["KEY"] = val

Sets an environment variable to String(value) through setenv, compound assignment included (process.env.X += …); a Symbol throws TypeError. A written variable is inherited by child processes, and delete process.env.KEY unsets it.

Returns

void — Nothing.

process.env.GREETING = "hi"
See it in a full example →

process.env as a value

Works, with caveats
Object.keys(process.env); for (const k in process.env); { ...process.env }; const env = process.env

The bare process.env is the live environment block as a dynamic object — a copy of environ on POSIX, GetEnvironmentStringsW on Windows (the hidden =X: per-drive entries skipped, key case kept) — so every enumeration form works and every value is a string.

for (const [k, v] of Object.entries(process.env)) {
  if (k.startsWith("MYAPP_")) console.log(k, v)
}
const child = { ...process.env, NODE_ENV: "production" }
See it in a full example →
⚠ Differences from standard
  • The object is a snapshot taken when the bare process.env expression is evaluated: keyed reads/writes (process.env.X) stay live, but a write through an alias (const env = process.env; env.X = '1') lands in the copy, not the environment — Node's object is a live proxy.

process.on(signal, listener)

Exact
process.on(signal: NodeJS.Signals, listener: (signal: NodeJS.Signals) => void): NodeJS.Process

Any signal is an event name — SIGINT/SIGTERM to shut down gracefully, SIGWINCH (terminal resize) to re-read process.stdout.columns/.rows and repaint a TUI, SIGHUP, SIGUSR1/2, … The first listener starts a watcher and removing the last restores the default disposition; SIGKILL/SIGSTOP throw uv_signal_start EINVAL. On Windows, as in Node, SIGINT fires on Ctrl+C, SIGBREAK on Ctrl+Break, and a SIGTERM listener is accepted but never fires.

Returns

NodeJS.Process — process, for chaining.

process as an EventEmitter ('exit'/'uncaughtException'/'unhandledRejection'/'warning') + exitCode

Works, with caveats
process.on('exit', (code) => …); process.exitCode = N

process is an EventEmitter: on/once/off/emit/listenerCount and the rest, any event name. 'exit' fires at normal end and from process.exit(), receiving the exit code, and a listener may set process.exitCode; process.exit() with no argument uses it. 'uncaughtException' receives the thrown value and its origin. A rejected promise nothing handles is reported after the microtask checkpoint, as Node's default does: 'unhandledRejection' receives (reason, promise), else it is an uncaught exception.

Returns

void — Registers a lifecycle listener.

process.on('exit', (code: number) => console.log("exiting with code", code))
process.exitCode = 0
See it in a full example →
⚠ Differences from standard
  • After a synchronous throw, 'uncaughtException' runs the listener (suppressing the default print) but the process still exits (code 1) — the setjmp/longjmp model has already unwound to the top-level catch, so it can't resume like Node.

process.hrtime() / .bigint()

Exact
process.hrtime(prev?: [number, number]): [number, number]; process.hrtime.bigint(): bigint

CLOCK_MONOTONIC high-resolution timing. hrtime() returns a [seconds, nanoseconds] tuple; hrtime(prev) returns the elapsed diff from a prior reading (with Node's borrow), and rejects a non-array or wrong-length argument as Node does; .bigint() returns total-nanoseconds as a bigint.

Returns

[number, number] | bigint — The timestamp or elapsed diff.

const start = process.hrtime()
// ...work...
const [s, ns] = process.hrtime(start)
See it in a full example →

process.getuid() / geteuid() / getgid() / getegid()

Exact
process.getuid(): number; geteuid(); getgid(); getegid()

Direct POSIX credential reads returning a number, matching Node-on-POSIX. Node leaves all four undefined on Windows: there the bare call is rejected at compile time in typed code and throws Node's TypeError when reached in untyped JS, and the portable optional call process.getuid?.() evaluates to undefined — it is typed number | undefined on every host.

Returns

number — The uid/gid.

process.memoryUsage()

Works, with caveats
process.memoryUsage(): { rss: number; heapTotal: number; heapUsed: number; external: number; arrayBuffers: number }

Returns Node's object shape. rss is the process's instantaneous resident set size (Darwin task_info, Linux /proc/self/statm), matching Node's live rss. heapTotal/heapUsed report this compiler's own object heap — Boehm's collected heap under -mm=gc, otherwise the C allocator's arena (Darwin malloc_zone_statistics, glibc mallinfo2) — so they are real and grow with allocation, a native reinterpretation rather than V8's number. external/arrayBuffers have no native off-heap analogue and stay 0.

Returns

object — Memory-usage byte counts; rss and heapTotal/heapUsed are populated, external/arrayBuffers are 0.

const m = process.memoryUsage(); console.log(m.rss > 0)
See it in a full example →

process.nextTick(fn)

Works, with caveats
process.nextTick(fn: () => void): void

Enqueues fn on the existing microtask queue.

Returns

void — Nothing.

process.nextTick(() => console.log("ran after synchronous code"))
See it in a full example →
⚠ Differences from standard
  • Shares the one microtask FIFO with Promise reactions rather than draining strictly before them (Node keeps a separate nextTick queue ahead of promises); zero-argument () => void callback only.

process.version / process.versions

Works, with caveats
process.version: string; process.versions: object

process.version = "v22.11.0"; process.versions is a real object (JSON.stringify/field access/Object.keys all work), matching Node v22.11.0's node/v8 verbatim, plus a klain key for this compiler's version — stamped at link time from the release tag (the same value `klainmain --version` prints).

Returns

string | object — Version string / versions object.

console.log(process.version, process.versions.klain)
See it in a full example →
⚠ Differences from standard
  • versions reports only honest values — node/v8/klain — deliberately omitting bundled-lib keys this compiler doesn't ship (uv/undici/icu/…) rather than fabricate them.
  • The real linked-library versions (openssl/zlib) are a follow-on — they need a runtime lib query.
  • node/v8 track the compatibility *ceiling* (the pinned test-corpus release); a --node-compat floor is deferred.

process.uptime()

Exact
process.uptime(): number

Seconds (a double) since process start, captured once at main entry via CLOCK_MONOTONIC.

Returns

number — Seconds since start.

process.umask([mask])

Exact
process.umask(): number; process.umask(mask: number): number

The process's file-mode creation mask. With no argument it reads the mask the way libuv does (set 0, restore); with one (a number, or an octal string such as '022') it sets it and returns the previous value. POSIX umask(2), so a file created afterwards carries the mask; on Windows it is the CRT's _umask — 0 by default and inert for created files, exactly Node's own answer there.

Returns

number — The current (or previous) mask, e.g. 0o022.

const prev = process.umask(0o077)
fs.writeFileSync('secret', '…') // mode 600 on POSIX
process.umask(prev)

process.stdin (streamed)

Works, with caveats
process.stdin.setEncoding('utf8'): typeof process.stdin; process.stdin.on('data' | 'end', cb): typeof process.stdin

Node's stdin stream: a tty.ReadStream on a terminal, otherwise a net.Socket over the descriptor (a pipe, a socket or a file). It starts paused; a 'data' listener, resume(), pipe() or for await starts it, and pause() stops reading so the process can exit. 'data' delivers Buffers unless setEncoding is called; read()/'readable' and several listeners per event work as on any Readable.

Returns

typeof process.stdin — The stream, for chaining.

process.stdin.setEncoding("utf8")
  .on("data", (chunk: string) => process.stdout.write(chunk.toUpperCase()))
  .on("end", () => console.error("done"))
See it in a full example →
⚠ Differences from standard
  • 'data' delivers each chunk as a UTF-8 string, not a Buffer — setEncoding('utf8') is a faithful no-op but a non-utf8 encoding is rejected (no binary mode).
  • Flowing mode only (attaching 'data' starts it) — no .pause()/.resume()/.read()/'readable'.
  • One listener per event, arrow/function-expression literal only; callbacks fire synchronously from the dispatch, not via a microtask.

readline module

Works, with caveats
readline.createInterface({ input }); rl.on('line' | 'close'); rl.question(q, cb); rl.close()

Node's readline over any Readable — stdin, a file stream, a child's stdout, a socket: 'line' per line (\r\n, \n, \r and the Unicode separators end one), 'close' at the end, question() with an AbortSignal, prompt/setPrompt, pause/resume, and for await (const line of rl). On a terminal it edits the line as Node does: cursor and word movement, kill ring, undo, history and tab completion. readline/promises' question returns a promise.

Returns

Interface — The readline interface.

⚠ Differences from standard
  • Listener callbacks fire synchronously from the event loop's dispatch, not deferred through a microtask queue.
  • One listener per event, an arrow/function-expression literal only; no line history/editing, no SIGINT/'pause'/'resume', no completer.
  • The createInterface options object is accepted but ignored — input is always stdin, output always stdout; one active interface at a time.

child_process.spawn / exec / execFile (async)

Works, with caveats
spawn(cmd, args?, opts?); exec(cmd, cb); execFile(file, args?, cb)

fork()+execvp() with three pipes; the read ends are non-blocking and fold into the same select() event loop as Worker/channel pipes. spawn streams (child.stdout/stderr 'data'/'end', child.on('close'/'exit'/'error'), child.stdin.write/.end, child.pid, child.kill); exec runs via /bin/sh -c and execFile execvp's directly, both buffering into one (err, stdout, stderr) callback.

Returns

ChildProcess — The child handle.

⚠ Differences from standard
  • Listener callbacks fire synchronously from the event loop's dispatch, not deferred through the microtask queue.
  • One listener per event, an arrow/function-expression literal only; no removeListener.
  • spawn's options support { cwd, shell, stdio: 'pipe', env } — cwd is real; shell: true routes the command through the platform shell (/bin/sh -c / cmd.exe /d /s /c, honouring %ComSpec%) as one string (a shell path, a dynamic value, or shell:true + an args array are clean rejections); env (an object literal of string values) fully replaces the child's environment, Node's non-merging semantics; windowsHide (literal bool) suppresses the child's console window on Windows (CREATE_NO_WINDOW), a no-op on POSIX; timeout (ms) has the event loop kill the child with killSignal (a literal signal name/number, default SIGTERM), surfacing as the (null, '<signal>') exit event; detached (literal bool) makes the child a new session/group leader — setsid on POSIX, DETACHED_PROCESS on Windows; child.unref()/ref() drop or restore the child's hold on the event-loop keepalive; stdio takes 'pipe'/'inherit'/'ignore' as a string or a 3-element [stdin, stdout, stderr] array — inherit wires the fd to the parent's, ignore sends it to /dev/null/NUL (fd numbers, 'ipc' and streams are clean rejections). exec(cmd[, options], cb) / execFile(file[, args][, options], cb) take the same options plus maxBuffer (1 MiB default; past it the child is killed and the callback gets RangeError [ERR_CHILD_PROCESS_STDIO_MAXBUFFER]) and a shell path; a timeout/kill error carries killed: true and the signal name, and an (err: any) parameter sees a real boxed Error.
  • The exec/execFile callback Error's code is null — Node's is the numeric exit code, which the string-typed code slot cannot carry; cmd is absent.
  • exec/execSync hand the command to the shell verbatim (embedded quotes survive); on Windows a .bat/.cmd spawned without a shell is refused, surfacing as the spawn 'error' event (EINVAL), closing the CVE-2024-27980 injection path.
  • A spawn that fails to start (a missing command → ENOENT) emits 'error' with an Error and not 'exit'; the Error carries .message, .code ('ENOENT' — the canonical e.code === 'ENOENT' guard) and .errno (libuv's errno: -2 on POSIX, -4058 on Windows). .syscall/.path/.spawnargs and Node's spawn <file> <CODE> message wording (which need the command string) are still unset.
  • 'close'/'exit' fire as (code, signal) like Node: a signal death gives code = null and the signal name ('SIGTERM'), a normal exit the integer code and signal = null. POSIX recovers the signal from the wait status; Windows from the signal child.kill recorded. A legacy listener with a single non-nullable number param sees 0 on a signal death (type it number | null for the faithful null).
  • The exit code is the child's real width — the full 32-bit value on Windows (cmd /c exit 300 → 300), the 8-bit wait-status field on POSIX (300 & 0xff = 44, as Node also reports there).
  • child.kill takes a signal name string only as a literal (resolved to the host's signal number at compile time) or a number; a dynamic signal-name string is a clean rejection.
  • Handles and their buffered output are never freed (-mm=manual).

child_process.fork (self-fork + IPC)

Works, with caveats
fork(modulePath, args?): ChildProcess

Node's own mechanism: socketpair + re-exec of the current binary with NODE_CHANNEL_FD set; json serialization, newline-framed. Parent: child.send(msg, cb?), child.on('message'/'exit'/'close'), child.disconnect(); child: process.send(msg, cb?), process.on('message'/'disconnect'), process.disconnect(), process.connected. child.send(msg, server | socket) passes a net.Server or net.Socket to the other process, which gets its own. process.send is undefined in a program that was not forked.

Returns

ChildProcess — The forked child with an IPC channel.

⚠ Differences from standard
  • Self-fork only: the module must be this program's entry (forking a different module is ERR_CHILD_PROCESS_FORK_MODULE).
  • serialization: 'advanced' is not ported: messages always cross as json.
  • A dgram socket cannot be sent as a handle; on Windows no handle can yet.

child_process.spawnSync / execSync / execFileSync

Exact
spawnSync(cmd, args?, opts?): { status, signal, output, pid, stdout, stderr, error? }; execSync(cmd, opts?): Buffer | string; execFileSync(file, args?, opts?): Buffer | string

A native blocking spawn on its own libuv loop, as Node's spawn_sync is (uv_spawn): the piped stdio read to completion (no deadlock when a child fills stderr while stdout drains), an optional input fed to stdin, a timeout/maxBuffer kill, then the exit; execSync runs via /bin/sh -c (%ComSpec% /d /s /c on Windows). Node's full options set: cwd, env (replaces the child's), input (string or Uint8Array), argv0, stdio ('pipe'/'inherit'/'ignore', string or 3-element array), timeout + killSignal (error.code ETIMEDOUT), maxBuffer (1 MiB default, ENOBUFS on overrun), shell (true or a shell path; args join the command with spaces), windowsHide, windowsVerbatimArguments. spawnSync returns Node's record in Node's key order — status is null on a signal death or a never-started child, output is [null, stdout, stderr], error carries code/errno/syscall 'spawnSync <file>'/path for a failed spawn (ENOENT: status null, pid 0, null streams). execSync/execFileSync return stdout — a Buffer, or a string with encoding: 'utf8' — or throw: the spawn/timeout/maxBuffer error itself, or 'Command failed: <cmd>' plus the captured stderr, the Error carrying the ExecException own properties status, signal, output, pid, stdout, stderr (readable through catch (e: any)); without an explicit stdio the child's stderr is mirrored to ours, as in Node.

Returns

SpawnSyncResult | string — The result object / captured stdout.

const r = spawnSync("echo", ["kalimera", "kosme"])
console.log(r.status, r.stdout.trim())
See it in a full example →
⚠ Differences from standard
  • An unpiped stdout/stderr (stdio: 'inherit'/'ignore') is null at run time while the field stays typed string (Node's own typings do the same); output is typed (string | null)[].