118: Process, Environment Variables, CLI Input/Output, Signals, and Exit Codes
Learning objectives
You will learn to:
- use
process.argv,process.env,stdin,stdout, andstderr; - build a well-behaved CLI;
- distinguish configuration from secrets and defaults;
- validate environment variables at startup;
- understand current working directory and process metadata;
- handle Unix-like process signals conceptually;
- set exit codes correctly;
- understand signal portability;
- avoid leaking secrets in logs;
- use AbortController to coordinate shutdown inside application code.
process
Node exposes information and controls for the current process:
console.log(process.pid);
console.log(process.cwd());
console.log(process.platform);
console.log(process.arch);
console.log(process.env.NODE_ENV);
Treat process as runtime infrastructure, not a global dumping ground.
Command-line arguments
Run:
node cli.js add "Write docs" --priority high
process.argv contains strings.
console.log(process.argv);
Typical shape:
[
'/path/to/node',
'/path/to/cli.js',
'add',
'Write docs',
'--priority',
'high'
]
Application args usually begin at:
const args = process.argv.slice(2);
For non-trivial CLIs, use a mature parser such as Commander, yargs, or another reviewed package rather than writing an ambiguous parser.
First understand raw argv.
Simple command parser
const [command, ...rest] = process.argv.slice(2);
switch (command) {
case 'list':
console.log('Listing tasks');
break;
case 'add': {
const title = rest.join(' ').trim();
if (!title) {
console.error('Usage: task add <title>');
process.exitCode = 2;
break;
}
console.log(`Adding: ${title}`);
break;
}
default:
console.error('Unknown command');
process.exitCode = 2;
}
This is sufficient for learning, not a production-grade flag parser.
stdout and stderr
Output data:
process.stdout.write('result\n');
Diagnostics/errors:
process.stderr.write('invalid input\n');
Why separate them?
A user can pipe data output:
task-cli list > tasks.txt
while errors still appear in the terminal.
A CLI that prints all diagnostics to stdout becomes difficult to compose in shell pipelines.
console.log
console.log() usually targets stdout.
console.error() targets stderr.
For structured production logging, use a logger designed for machine-readable logs rather than relying on ad-hoc console statements.
Reading stdin
process.stdin.setEncoding('utf8');
let input = '';
for await (const chunk of process.stdin) {
input += chunk;
}
console.log(input.trim());
Because stdin is a stream, async iteration works naturally.
Later you will study stream backpressure and transformations.
TTY versus pipe
Check:
process.stdin.isTTY
process.stdout.isTTY
Interactive terminal behavior can differ from piped input.
Example:
TTY → show prompts/colors/progress pipe → emit clean machine-readable output
Do not send ANSI color codes into JSON redirected to a file unless explicitly requested.
Environment variables
Read:
const port = process.env.PORT;
Environment values are strings or undefined.
This:
const port = process.env.PORT || 3000;
returns a string when configured.
Parse and validate:
function readPort() {
const raw = process.env.PORT ?? '3000';
const value = Number(raw);
if (!Number.isInteger(value) || value < 1 || value > 65535) {
throw new Error(`Invalid PORT: ${raw}`);
}
return value;
}
Validate configuration once
Create:
// config.js
function required(name) {
const value = process.env[name];
if (!value) {
throw new Error(`Missing required environment variable: ${name}`);
}
return value;
}
export const config = Object.freeze({
port: Number(process.env.PORT ?? 3000),
databaseUrl: required('DATABASE_URL'),
nodeEnv: process.env.NODE_ENV ?? 'development',
});
Improve it by validating every type/range.
Libraries such as Zod/Valibot can validate config schemas, but do not make configuration validation dependent on request-time code.
Fail at startup.
.env files
Environment-file tooling is convenient in local development.
Modern Node releases also include environment-file capabilities/CLI options; verify your target Node version.
A .env file is not a production secret manager.
Never commit real secrets:
DATABASE_URL with production password JWT signing secret private API credentials
Use platform secret/config facilities.
Add local files to .gitignore where appropriate.
Configuration categories
Useful classification:
Code constant
MAX_USERNAME_LENGTH = 80
Business/program invariant.
Environment config
PORT LOG_LEVEL DATABASE_URL
Deployment-specific.
Secret
DB_PASSWORD OAUTH_CLIENT_SECRET SIGNING_KEY
Sensitive deployment value.
Runtime feature flag
May come from dedicated configuration/feature service.
Do not put every changing business rule into environment variables.
Secret logging
Never:
console.log(process.env);
in production.
That can leak:
- database passwords;
- cloud credentials;
- tokens;
- signing keys.
Redact structured logs.
Current working directory
console.log(process.cwd());
Change:
process.chdir('/tmp');
Avoid changing cwd in application servers unless you have a strong reason; it changes relative path behavior process-wide.
Resolve application resources relative to the module when appropriate, not assumed cwd.
Exit codes
Set:
process.exitCode = 1;
and allow normal event-loop completion.
process.exit(code) exits immediately.
Danger:
process.stdout.write(largeOutput);
process.exit(0);
Output may be truncated.
For a CLI, set exitCode and return from top-level flow.
Top-level CLI pattern
async function main() {
const [command] = process.argv.slice(2);
if (!command) {
throw new Error('Command required');
}
...
}
try {
await main();
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}
This creates one clear error boundary.
Signals
Long-running services receive operating-system signals.
Common Unix signals include:
SIGTERM SIGINT
SIGINT often comes from Ctrl+C.
Container/process managers commonly use SIGTERM for graceful stop.
process.once('SIGTERM', () => {
console.log('received SIGTERM');
});
process.once('SIGINT', () => {
console.log('received SIGINT');
});
Signal behavior differs across operating systems. Do not assume every Unix signal exists identically on Windows.
Graceful shutdown concept
signal → stop accepting new work → abort/cancel background work where appropriate → finish/timeout in-flight requests → close database/message connections → flush logs → set exit status / let process exit
A later production lesson implements this fully.
AbortController for shutdown
const shutdownController = new AbortController();
process.once('SIGTERM', () => {
shutdownController.abort(new Error('shutdown'));
});
Pass:
shutdownController.signal
to internal jobs that support cancellation.
This creates a composable shutdown signal rather than a global boolean checked everywhere.
beforeExit and exit
Node process events exist, but do not use them as a general graceful-shutdown mechanism.
The exit event is too late for ordinary asynchronous cleanup.
Shutdown should begin when receiving a signal or application failure condition.
Uncaught fatal errors
Do not make:
process.on('uncaughtException', error => {
console.error(error);
// continue forever
});
your recovery strategy.
After an uncaught exception, application invariants may be compromised.
A production service should:
- log/observe safely;
- begin controlled shutdown if possible;
- let a process supervisor restart it.
The dedicated error/production lessons go deeper.
CLI project: environment checker
const required = [
'DATABASE_URL',
'APP_ENV',
];
const missing = required.filter((name) => !process.env[name]);
if (missing.length) {
console.error(`Missing: ${missing.join(', ')}`);
process.exitCode = 1;
} else {
console.log('Configuration OK');
}
Notice it does not print secret values.
Interactive prompts
For a polished interactive CLI, use packages such as Inquirer/prompts when justified.
The parser/prompt layer should return plain domain values:
{
title: 'Review Node',
priority: 'high'
}
so core logic is testable without a terminal.
Common mistakes
- parsing every flag manually in a large CLI;
- printing errors to stdout;
- assuming env vars have number/boolean types;
- committing secrets;
- logging whole environment;
- using process.exit immediately after async output;
- running async cleanup from
exit; - assuming SIGTERM semantics are identical on every platform;
- making global process state the business layer.
Exercises
- Build
task listandtask addargv parsing. - Separate data output and diagnostic output.
- Support piped stdin.
- Validate PORT and DATABASE_URL.
- Add
--jsonoutput and ensure diagnostics remain stderr. - Set meaningful exit codes.
- Add SIGINT handling that triggers an AbortController.
- Explain what happens if a server ignores SIGTERM in a container platform.
Mastery checklist
Explain:
- argv;
- stdin/stdout/stderr;
- TTY;
- environment strings;
- config validation;
- secrets;
- exitCode versus exit();
- signals;
- graceful-shutdown lifecycle;
- AbortController as cancellation infrastructure.
