process is a global object in Node — no import needed. It’s the bridge between our JavaScript code and the operating system: command-line arguments, environment variables, exit codes, signals, the current working directory. Every real Node app uses it constantly.
In simple language: process is “how Node sees the outside world.” Anything that came from the shell that ran us — args, env vars, stdin — lives here.
process.argv — command-line arguments
When we run node script.js --port 3000 --debug, the args show up here. The catch: the first two entries are always the Node binary path and the script path.
console.log(process.argv);
// [ '/usr/bin/node', '/app/script.js', '--port', '3000', '--debug' ]
const args = process.argv.slice(2);
// [ '--port', '3000', '--debug' ]
For anything beyond trivial parsing, reach for the built-in node:util.parseArgs (Node 18.3+) or libraries like commander / yargs.
import { parseArgs } from 'node:util';
const { values } = parseArgs({
options: {
port: { type: 'string', default: '3000' },
debug: { type: 'boolean', default: false },
},
});
// values.port === '3000', values.debug === false
process.env — environment variables
Every env var is a string. process.env.PORT is "3000", not the number 3000. Convert deliberately.
const port = parseInt(process.env.PORT ?? '3000', 10);
const debug = process.env.DEBUG === 'true';
The ?? handles the unset case — process.env.SOMETHING_UNSET is undefined.
The dotenv pattern
We don’t want to type PORT=3000 DATABASE_URL=... node app.js every time. The convention: keep secrets in a .env file (gitignored) and load it at startup.
# .env
PORT=3000
DATABASE_URL=postgres://localhost/mydb
ANTHROPIC_API_KEY=sk-ant-...
Old-school way — the dotenv package:
import 'dotenv/config';
// now process.env.PORT, process.env.DATABASE_URL etc. are populated
Node 20.6+ ships this natively. No dependency needed:
node --env-file=.env app.js
process.exit and exit codes
process.exit(0) says “success,” anything non-zero is failure. Shell scripts and CI pipelines check these codes.
if (!process.env.DATABASE_URL) {
console.error('FATAL: DATABASE_URL required');
process.exit(1);
}
The gotcha: process.exit is abrupt. Pending writes to stdout/stderr can get cut off. For graceful shutdown, set process.exitCode = 1 and let the event loop drain naturally.
process events — graceful shutdown
When Kubernetes sends SIGTERM or we hit Ctrl+C (SIGINT), we should close DB connections, finish in-flight requests, then exit. The pattern:
async function shutdown(signal) {
console.log(`Received ${signal}, shutting down...`);
await server.close();
await db.end();
process.exit(0);
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
The two events nobody should ignore:
uncaughtException— a thrown error nothing caught. State is unknown, log it and exit.unhandledRejection— a promise rejected with no.catch. Same deal.
process.on('uncaughtException', (err) => {
console.error('Uncaught:', err);
process.exit(1);
});
process.on('unhandledRejection', (reason) => {
console.error('Unhandled rejection:', reason);
process.exit(1);
});
Trying to “recover” from these is almost always wrong — the app is in an undefined state.
The other useful bits
process.cwd(); // current working directory
process.chdir('/tmp'); // change it (rarely needed)
process.pid; // process ID — useful in logs
process.platform; // 'darwin' | 'linux' | 'win32'
process.version; // 'v20.10.0' — Node version
process.uptime(); // seconds since process started
process.memoryUsage(); // { rss, heapTotal, heapUsed, ... }
process.memoryUsage() is gold for debugging memory leaks. Log heapUsed periodically and watch the trend.
The mental model
process is the OS-facing side of Node. Parse argv for CLI args, read env for config (always as strings), exit cleanly with proper codes, and always handle SIGTERM if you ship anything to production — otherwise rolling deploys will drop in-flight requests.