process & env
The process object
Section titled “The process object”process is a global object that gives your program access to the running Node.js process. It is available everywhere — no import required.
The most commonly used properties are:
| Property / Method | What it gives you |
|---|---|
process.argv | Array of command-line arguments |
process.env | Object of environment variables |
process.cwd() | Current working directory string |
process.exit(code) | Terminate the process with an exit code |
process.platform | OS platform string ('linux', 'darwin', 'win32') |
process.version | Node.js version string (e.g. 'v20.11.0') |
process.argv — command-line arguments
Section titled “process.argv — command-line arguments”process.argv is always an array. The first two elements are fixed:
argv[0]— path to thenodebinaryargv[1]— path to the script being runargv[2]onwards — your actual arguments
Needs the Node.js runtime — open in StackBlitz to run.
process.env — environment variables
Section titled “process.env — environment variables”process.env is a plain object whose keys and values are strings. It is populated from the shell environment at process startup.
Needs the Node.js runtime — open in StackBlitz to run.
Every value is a string — the PORT > 1024 trap
Section titled “Every value is a string — the PORT > 1024 trap”Every key and value in process.env is a string, even if the shell set it to something that looks like a number or a boolean. PORT=3000 in the environment gives you process.env.PORT === '3000', not 3000. Relational operators (<, >) coerce a string operand to a number for you, but === and + do not — that mismatch is one of the most common process.env bugs:
Output (verified on Node 24)
typeof PORT: string
PORT === 3000: false
PORT === "3000": true
PORT + 1: 30001
Number(PORT) + 1: 3001
PORT > 1024: true
unset PORT > 1024: false
Number(unset PORT): NaNLoading a .env file without dotenv
Section titled “Loading a .env file without dotenv”Since Node 22 the runtime loads env files itself — from the CLI with --env-file, or from code with process.loadEnvFile():
node --env-file=.env src/app.jsimport { writeFileSync, unlinkSync } from 'node:fs';import { loadEnvFile } from 'node:process';
writeFileSync('.env', 'PORT=4000\n'); // demo fixtureloadEnvFile(); // defaults to ./.envconsole.log(process.env.PORT);unlinkSync('.env');process.cwd() and process.platform
Section titled “process.cwd() and process.platform”Needs the Node.js runtime — open in StackBlitz to run.
process.exit()
Section titled “process.exit()”process.exit(code) terminates the process immediately. By convention:
- Exit code
0means success. - Any non-zero code (typically
1) means failure.
// @expect-throw// Example: exit with error if a required env var is missingif (!process.env.DATABASE_URL) { console.error('ERROR: DATABASE_URL is required'); process.exit(1);}Calling process.exit() is a hard stop — it skips pending async callbacks and any finally blocks still on the call stack. Prefer setting process.exitCode (e.g. process.exitCode = 1) and letting the process exit naturally instead — that gives pending I/O, finally blocks, and a graceful shutdown (e.g. server.close()) a chance to run.
Output (verified on Node 24)
exitCode set to 1 (tentative)
setImmediate callback ran -- exitCode does NOT skip pending callbacksOutput (verified on Node 24)
about to hard-exit with process.exit(1)Same pending setImmediate, two different outcomes: process.exitCode lets it run before the process exits; process.exit() cuts it off mid-flight.
Signals and graceful shutdown
Section titled “Signals and graceful shutdown”Orchestrators (Docker, Kubernetes, systemd, Ctrl+C in a terminal) stop a process by sending it a signal, not by killing it outright: SIGTERM asks nicely, SIGINT is what Ctrl+C sends. Node lets you intercept both with process.on() to finish in-flight work — stop accepting new connections, let existing requests finish, close database connections — before actually exiting:
Output (verified on Node 24)
server started (simulated)
this line runs synchronously -- the signal handler fires after it
SIGTERM received -- closing server gracefully
cleanup done, exiting 0process.nextTick — where it fits in the event loop
Section titled “process.nextTick — where it fits in the event loop”process.nextTick(fn) queues fn to run before the event loop continues to the next phase — even before other microtasks (promise callbacks) queued earlier. It’s covered in depth, with the full ordering against Promise, setImmediate, and setTimeout, in Microtasks and Macrotasks. The short version relevant here: process is where nextTick lives, but it is really an event-loop primitive, not a process-specific feature — it exists on process for historical reasons (it predates queueMicrotask).
The node: prefix — recap
Section titled “The node: prefix — recap”As covered in this module’s overview, process needs no import at all — it’s a true global, like Buffer. Every module used on this page (node:util, node:fs, node:child_process) can be imported with or without the node: prefix if it’s a legacy module; brand-new built-ins like node:test and node:sqlite only resolve with the prefix. When you see require('util') in older code, it is equivalent to require('node:util') — the prefix is a clarity convention, not a behavior change, for anything that predates it.
CLI parsing and terminal styling: util.parseArgs and util.styleText
Section titled “CLI parsing and terminal styling: util.parseArgs and util.styleText”node:util ships two small utilities that remove the need for popular npm packages in simple CLI tools. parseArgs (stable since 20.x) replaces yargs/minimist for straightforward flag parsing:
Output (verified on Node 24)
[Object: null prototype] { name: 'Ada', verbose: true, count: '3' }styleText (stable since 22.x) adds ANSI color/style codes to a string — replacing chalk/picocolors for basic cases. It automatically checks whether the target stream actually supports color and returns the plain string unchanged if not (piped output, a log file, most CI runners):
Output (verified on Node 24)
"OK"
length with codes: 2 vs plain length: 2Mental Model
Section titled “Mental Model”flowchart TB Env["process.env.PORT"] -->|"always"| Str["a string, never a number"] Str -->|"=== or arithmetic"| Trap["silent bug: false / string concat"] Str -->|"Number() first"| Safe["correct comparison/arithmetic"] Exit1["process.exitCode = n"] -->|"lets pending callbacks + finally run"| Natural["process exits naturally"] Exit2["process.exit(n)"] -->|"cuts off immediately"| Hard["pending callbacks + finally skipped"] Sig["SIGTERM / SIGINT"] -->|"no listener"| Immediate["process dies immediately, mid-request"] Sig -->|"process.on(signal, ...)"| Graceful["finish in-flight work, then exit(0)"]
Trade-offs
Section titled “Trade-offs”| Choice | Benefit | Cost |
|---|---|---|
Reading config via process.env | Same code deploys to every environment (12-Factor); no rebuild for new config | Everything is a string — every numeric/boolean value needs explicit conversion and validation |
process.exitCode = n | Pending I/O, timers, and finally blocks get to run; safer default | Requires the event loop to actually drain — a stuck handle keeps the process alive |
process.exit(n) | Guarantees immediate termination, useful when something is truly stuck | Skips finally blocks and pending callbacks unconditionally — including ones flushing logs or closing connections |
Handling SIGTERM yourself | Requests in flight get to finish; connections close cleanly | Adds shutdown-path code that’s easy to get wrong (must actually call exit() eventually, or the orchestrator’s grace period runs out and it SIGKILLs you anyway) |
Common Misconceptions
Section titled “Common Misconceptions”- “process.env.PORT > 1024 is unsafe because PORT is a string” — fact: relational operators (
</>) coerce a string operand to a number automatically; the real traps are===(never coerces) and+(concatenates strings instead of adding). - “process.exit() and setting process.exitCode do the same thing, just different syntax” — fact:
exitCodelets the event loop drain naturally — pending callbacks andfinallyblocks run;exit()terminates immediately, skipping both. - “Without a signal handler, Kubernetes waits for my process to finish its current request” — fact: with no
SIGTERMlistener, the default behavior terminates the process right away; only an explicit handler buys time to finish in-flight work, and only up to the orchestrator’s grace period. - “styleText always adds color codes to my string” — fact: it checks whether the destination stream supports color first, and silently returns the plain string when it doesn’t (piped output, log files, most CI).
Reading AI-generated code
Section titled “Reading AI-generated code”- Does code compare
process.env.SOMETHINGwith===against a number, or feed it directly into arithmetic with+, without an explicitNumber(...)/parseInt(...)conversion first? - Does a script call
process.exit()right afterconsole.log()or afs.writeFile()/database write, when the write might still be in flight? On some platforms buffered stdout can be truncated, and any pending async write is abandoned outright. - Does a long-running server register a
SIGTERM/SIGINThandler at all? Its absence means every deploy kills in-flight requests instead of finishing them. - Does cleanup logic live in a
finallyblock that only runs on the natural exit path, while an earlier branch callsprocess.exit()directly — silently skipping that same cleanup?
🐛 Spot the bug
import http from 'node:http';
var server = http.createServer(function(req, res) {res.end('ok');});
server.listen(process.env.PORT || 3000, function() {console.log('listening on port', process.env.PORT || 3000);});Show the answer
process.env.PORT is a string (or undefined). server.listen() happens to accept a string port number, so this specific call doesn’t crash — but the moment this value is used anywhere with ===, arithmetic, or a type check (typeof port === 'number', a validation schema, a health-check comparing against a numeric range), the string type causes a silent bug. The fix is to convert once, near the top: var port = Number(process.env.PORT) || 3000;, so every later use of port is a real number.