Skip to content

process & env

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 / MethodWhat it gives you
process.argvArray of command-line arguments
process.envObject of environment variables
process.cwd()Current working directory string
process.exit(code)Terminate the process with an exit code
process.platformOS platform string ('linux', 'darwin', 'win32')
process.versionNode.js version string (e.g. 'v20.11.0')

process.argv is always an array. The first two elements are fixed:

  • argv[0] — path to the node binary
  • argv[1] — path to the script being run
  • argv[2] onwards — your actual arguments
Node.js

Needs the Node.js runtime — open in StackBlitz to run.

process.env is a plain object whose keys and values are strings. It is populated from the shell environment at process startup.

Node.js

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:

Node.js
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): NaN

Since Node 22 the runtime loads env files itself — from the CLI with --env-file, or from code with process.loadEnvFile():

Terminal window
node --env-file=.env src/app.js
import { writeFileSync, unlinkSync } from 'node:fs';
import { loadEnvFile } from 'node:process';
writeFileSync('.env', 'PORT=4000\n'); // demo fixture
loadEnvFile(); // defaults to ./.env
console.log(process.env.PORT);
unlinkSync('.env');
Node.js

Needs the Node.js runtime — open in StackBlitz to run.

process.exit(code) terminates the process immediately. By convention:

  • Exit code 0 means success.
  • Any non-zero code (typically 1) means failure.
// @expect-throw
// Example: exit with error if a required env var is missing
if (!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.

Node.js
Output (verified on Node 24)
exitCode set to 1 (tentative)
setImmediate callback ran -- exitCode does NOT skip pending callbacks
Node.js
Output (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.

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:

Node.js
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 0

process.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).

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:

Node.js
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):

Node.js
Output (verified on Node 24)
"OK"
length with codes: 2 vs plain length: 2
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)"]
process.env is always strings; exitCode drains pending work while exit() cuts it off; an unhandled signal kills the process immediately, a handled one exits gracefully
ChoiceBenefitCost
Reading config via process.envSame code deploys to every environment (12-Factor); no rebuild for new configEverything is a string — every numeric/boolean value needs explicit conversion and validation
process.exitCode = nPending I/O, timers, and finally blocks get to run; safer defaultRequires the event loop to actually drain — a stuck handle keeps the process alive
process.exit(n)Guarantees immediate termination, useful when something is truly stuckSkips finally blocks and pending callbacks unconditionally — including ones flushing logs or closing connections
Handling SIGTERM yourselfRequests in flight get to finish; connections close cleanlyAdds 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)
  • “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: exitCode lets the event loop drain naturally — pending callbacks and finally blocks run; exit() terminates immediately, skipping both.
  • “Without a signal handler, Kubernetes waits for my process to finish its current request” — fact: with no SIGTERM listener, 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).
  • Does code compare process.env.SOMETHING with === against a number, or feed it directly into arithmetic with +, without an explicit Number(...)/parseInt(...) conversion first?
  • Does a script call process.exit() right after console.log() or a fs.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/SIGINT handler at all? Its absence means every deploy kills in-flight requests instead of finishing them.
  • Does cleanup logic live in a finally block that only runs on the natural exit path, while an earlier branch calls process.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.

What does process.argv[0] contain when you run node script.js?
What type are the values stored in process.env?
Which exit code conventionally indicates successful completion?
What does this print? process.env.PORT = "3000"; console.log(process.env.PORT === 3000); console.log(process.env.PORT > 1024);
A function does process.exit(1) immediately after starting a setImmediate(() => cleanup()) call. Does cleanup() run?
What happens to a Node.js server process that receives SIGTERM with no signal handler registered?