Module: Nodejs
Nodejs·118·4 MIN READ

118: Process, Environment Variables, CLI Input/Output, Signals, and Exit Codes

TOPICS COVERED: Process, Environment Variables, CLI Input/Output, Signals, and Exit Codes

Learning objectives

You will learn to:

  • use process.argv, process.env, stdin, stdout, and stderr;
  • 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:

js
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:

bash
node cli.js add "Write docs" --priority high

process.argv contains strings.

js
console.log(process.argv);

Typical shape:

js
[
  '/path/to/node',
  '/path/to/cli.js',
  'add',
  'Write docs',
  '--priority',
  'high'
]

Application args usually begin at:

js
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

js
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:

js
process.stdout.write('result\n');

Diagnostics/errors:

js
process.stderr.write('invalid input\n');

Why separate them?

A user can pipe data output:

bash
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

js
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:

js
process.stdin.isTTY
process.stdout.isTTY

Interactive terminal behavior can differ from piped input.

Example:

text
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:

js
const port = process.env.PORT;

Environment values are strings or undefined.

This:

js
const port = process.env.PORT || 3000;

returns a string when configured.

Parse and validate:

js
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:

js
// 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:

text
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

text
MAX_USERNAME_LENGTH = 80

Business/program invariant.

Environment config

text
PORT
LOG_LEVEL
DATABASE_URL

Deployment-specific.

Secret

text
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:

js
console.log(process.env);

in production.

That can leak:

  • database passwords;
  • cloud credentials;
  • tokens;
  • signing keys.

Redact structured logs.

Current working directory

js
console.log(process.cwd());

Change:

js
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:

js
process.exitCode = 1;

and allow normal event-loop completion.

process.exit(code) exits immediately.

Danger:

js
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

js
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:

text
SIGTERM
SIGINT

SIGINT often comes from Ctrl+C.

Container/process managers commonly use SIGTERM for graceful stop.

js
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

text
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

js
const shutdownController = new AbortController();

process.once('SIGTERM', () => {
  shutdownController.abort(new Error('shutdown'));
});

Pass:

js
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:

js
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

js
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:

js
{
  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

  1. Build task list and task add argv parsing.
  2. Separate data output and diagnostic output.
  3. Support piped stdin.
  4. Validate PORT and DATABASE_URL.
  5. Add --json output and ensure diagnostics remain stderr.
  6. Set meaningful exit codes.
  7. Add SIGINT handling that triggers an AbortController.
  8. 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.

Official references