The process Object
Every Node.js program has exactly one process object, and Node exposes it as a global — you never require or import it. It is your window into the operating-system process your script is running inside: the command-line arguments it was launched with, the environment variables available to it, the current working directory, memory usage, and the ability to exit cleanly or react to signals like Ctrl+C. Because process is also an EventEmitter, you can listen for lifecycle events such as unhandled errors or shutdown signals — the foundation for writing programs that shut down gracefully instead of dropping work mid-flight.
Overview: How process Works
process is not part of the JavaScript language — browsers don’t have it. It’s a global object that the Node.js runtime creates for you at startup, backed by libuv and the underlying operating system. Think of it as the bridge between your JavaScript code and the OS process (the same kind of process you’d see in ps or Task Manager) that node created when you ran your script.
A few of its most-used pieces:
- process.argv — an array of the command-line arguments. Index
0is always the path to thenodeexecutable, index1is the path to the script being run, and anything after that (from index2onward) is what the user actually typed. A common beginner mistake is forgetting the first two slots exist. - process.env — a plain object mirroring the environment variables the process inherited from its parent shell. Every value is a string, even things that look numeric or boolean, and changing
process.env.Xonly affects this process’s in-memory copy — it does not write back to the shell or the OS. - process.cwd() — the current working directory, i.e. the directory you were in when you typed
node. This is different from__dirname(the folder the running file lives in) — if you runnode src/app.jsfrom your project root,cwd()is the project root but__dirnameis.../src. - process.exit() and process.exitCode — two different ways to end the program.
process.exit()terminates immediately, even if there is unfinished asynchronous work (a file write, a pending timer); Node does not wait for the event loop to drain. Settingprocess.exitCodeinstead just records the exit code to use once the event loop naturally has nothing left to do — the safer default. - process.pid, process.platform (
'linux','darwin','win32'), process.arch, process.version and process.versions — identifying information about the running process and the Node build (V8 version, OpenSSL version, and so on). - process.stdout, process.stderr, process.stdin — these are streams.
console.logis, under the hood, mostly a formatted wrapper aroundprocess.stdout.write().
process also extends Node's EventEmitter, so you can call process.on(eventName, listener) to react to things happening to the process itself: it receiving a POSIX signal (SIGINT from Ctrl+C, SIGTERM from docker stop or kill), an error escaping every try/catch and every .catch() (uncaughtException, unhandledRejection), or the process about to end (exit, which only allows synchronous work — no more I/O can happen once it fires).
One more piece worth knowing early: process.nextTick(callback) schedules a callback to run before the event loop moves on to anything else — before even Promise microtasks, and long before timers or I/O. It's how Node's own internals defer work without letting an I/O event slip in first. The "Under the hood" section below shows exactly where it fits in the ordering.
Syntax
process is a global — there's no import or constructor call. You simply reference members on it:
process.argv // string[] — CLI arguments
process.env // object — environment variables (all strings)
process.cwd() // string — current working directory
process.chdir(path) // change the working directory
process.exit(code) // terminate immediately (code defaults to 0)
process.exitCode = code // request an exit code once the loop drains
process.pid // number — process ID
process.platform // 'linux' | 'darwin' | 'win32' | ...
process.version // Node version string, e.g. 'v22.10.0'
process.memoryUsage() // object with rss, heapTotal, heapUsed, external
process.hrtime.bigint() // bigint — high-resolution time in nanoseconds
process.nextTick(callback) // queue callback before the next event-loop phase
process.on(eventName, listener) // listen for 'exit', 'SIGINT', 'uncaughtException', ...
| Member | What it's for |
|---|---|
argv |
Read command-line arguments (index 2+ are user-supplied) |
env |
Read configuration/secrets injected by the shell, CI, or container |
exit event |
Last chance to log/clean up synchronously before the process ends |
SIGINT / SIGTERM |
Catch Ctrl+C or an orchestrator's stop signal to shut down gracefully |
uncaughtException |
Last-resort handler for a thrown error nothing else caught |
unhandledRejection |
Last-resort handler for a rejected promise nothing else caught |
Examples
Example 1: Inspecting the current process
console.log("PID:", process.pid);
console.log("Platform:", process.platform);
console.log("Node version:", process.version);
console.log("Architecture:", process.arch);
console.log("Current working directory:", process.cwd());
console.log("Command-line arguments:", process.argv);
Output:
PID: 41823
Platform: linux
Node version: v22.10.0
Architecture: x64
Current working directory: /home/user/project
Command-line arguments: [ '/usr/local/bin/node', '/home/user/project/app.js' ]
The exact PID, platform, and paths depend on your machine — run it yourself with node app.js and compare. Notice that process.argv always has at least two entries before any arguments you actually pass.
Example 2: Reading CLI arguments and required environment variables
const args = process.argv.slice(2);
const name = args[0] ?? "stranger";
const port = process.env.PORT ?? 3000;
if (!process.env.API_KEY) {
console.error("Missing required environment variable: API_KEY");
process.exitCode = 1;
} else {
console.log(`Hello, ${name}! Server would start on port ${port}.`);
}
node app.js Alice
API_KEY=secret123 node app.js Alice
Output:
Missing required environment variable: API_KEY
Hello, Alice! Server would start on port 3000.
The first run has no API_KEY set, so it prints an error and sets process.exitCode = 1 — the script keeps running the rest of its (synchronous) logic but will report a failing exit code to whatever launched it, without the abruptness of process.exit(). The second run supplies API_KEY inline before the command, which is how you pass one-off environment variables from a shell without editing any file. Notice args.slice(2) — that's how you strip off the node executable and script path to get just what the user typed.
Example 3: A server with graceful shutdown
const http = require("node:http");
const server = http.createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("OK\n");
});
const port = process.env.PORT ?? 3000;
server.listen(port, () => {
console.log(`Server listening on port ${port}`);
});
function shutdown(signal) {
console.log(`\nReceived ${signal}, closing server...`);
server.close(() => {
console.log("Server closed. Exiting.");
process.exitCode = 0;
});
}
process.on("SIGINT", () => shutdown("SIGINT"));
process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("unhandledRejection", (reason) => {
console.error("Unhandled promise rejection:", reason);
process.exitCode = 1;
});
process.on("uncaughtException", (err) => {
console.error("Uncaught exception:", err);
process.exitCode = 1;
});
Output (after starting it and pressing Ctrl+C):
Server listening on port 3000
^C
Received SIGINT, closing server...
Server closed. Exiting.
This is close to what you'd want in production: the server keeps running until it receives SIGINT (Ctrl+C) or SIGTERM (what Docker and most process managers send on stop), at which point server.close() stops accepting new connections but lets in-flight ones finish before the callback fires and the process exits. The two error-event handlers are a last line of defense — they make sure a stray thrown error or rejected promise gets logged and produces a non-zero exit code instead of the process crashing silently or hanging.
Under the Hood: Event Loop Ordering
process.nextTick() is one of the most misunderstood pieces of Node. It is not the same as setImmediate(), despite the similar-sounding names, and it does not go through the libuv event loop phases at all — it runs on a dedicated queue that is drained completely (including anything newly added to it) right after the current operation finishes, before Node even looks at Promise microtasks, let alone timers.
const fs = require("node:fs");
console.log("start");
fs.readFile(__filename, () => {
setTimeout(() => console.log("setTimeout"), 0);
setImmediate(() => console.log("setImmediate"));
});
process.nextTick(() => console.log("nextTick"));
Promise.resolve().then(() => console.log("promise"));
console.log("end");
Output:
start
end
nextTick
promise
setImmediate
setTimeout
Step by step: the synchronous code runs first, printing start and end (the fs.readFile call just kicks off an async read and returns immediately). Once the main script finishes, Node drains the nextTick queue, then the Promise microtask queue — that's why nextTick always beats promise. Only after both queues are empty does the event loop move into its phases: timers, then poll (where the file read's callback fires), then check. Because the setTimeout and setImmediate calls happen inside the fs.readFile callback — i.e. during the poll phase — the loop moves straight into the check phase next, running setImmediate before it wraps back around to timers for setTimeout. That ordering (setImmediate before setTimeout) is guaranteed whenever both are scheduled inside an I/O callback; at the very top level of a script, without an I/O callback involved, the order between a 0ms timer and setImmediate is not guaranteed and can vary between runs.
Common Mistakes
Mistake 1: Calling process.exit() while async work is still pending
const fs = require("node:fs");
fs.writeFile("log.txt", "important log line\n", (err) => {
if (err) console.error(err);
console.log("Log written");
});
process.exit(0);
process.exit() terminates the process immediately — it does not wait for the event loop to drain. The write to log.txt may never finish, and "Log written" may never print, because the process can end before that callback gets a chance to run. This is a frequent source of "it works on my machine but the log file is empty in production" bugs.
const fs = require("node:fs");
fs.writeFile("log.txt", "important log line\n", (err) => {
if (err) {
console.error(err);
process.exitCode = 1;
return;
}
console.log("Log written");
});
Setting process.exitCode instead lets the pending write finish naturally; Node exits on its own, with the exit code you specified, once there is truly nothing left to do. Reach for process.exit() only when you deliberately want to abort immediately (for example, in a CLI tool right after a fatal, unrecoverable startup error where no cleanup is needed).
Mistake 2: Not handling unhandledRejection
async function loadConfig() {
throw new Error("config file not found");
}
loadConfig();
console.log("Server starting...");
loadConfig() returns a rejected promise that nothing ever .catch()es. In current Node LTS versions the default behavior for an unhandled rejection is to print a warning and terminate the process — which can look like the app "randomly crashed" if you aren't watching for it. Never fire off an async function without handling its rejection.
process.on("unhandledRejection", (reason) => {
console.error("Unhandled rejection:", reason);
process.exitCode = 1;
});
async function loadConfig() {
throw new Error("config file not found");
}
loadConfig().catch((err) => {
console.error("Failed to load config:", err.message);
process.exitCode = 1;
});
console.log("Server starting...");
Add an explicit .catch() at every call site where you don't await a promise, and keep a top-level unhandledRejection listener as a safety net for anything that slips through — log it, set a non-zero exitCode, and let the process end predictably rather than in an undefined half-crashed state.
Best Practices
- Prefer
process.exitCode = noverprocess.exit(n)so pending I/O and timers get a chance to finish before the process actually ends. - Always register
process.on("unhandledRejection", ...)andprocess.on("uncaughtException", ...)in long-running programs — log the error, set a non-zero exit code, and let the process end deliberately rather than crash unpredictably or keep running with corrupted state. - Validate required
process.envvariables at startup and fail fast with a clear message, instead of discovering a missing config value deep inside a request handler. - Never log the entire
process.envobject — it frequently contains secrets (API keys, database URLs, tokens). - Remember every value in
process.envis a string; convert explicitly (Number(process.env.PORT)) rather than assuming numeric or boolean types. - Handle
SIGTERMas well asSIGINT— container orchestrators (Docker, Kubernetes) sendSIGTERMon shutdown, not Ctrl+C. - Only put synchronous code in an
exitevent listener — no more asynchronous work (timers, I/O) is allowed to run once that event fires. - Use
process.hrtime.bigint()(or theperf_hooksmodule) rather thanDate.now()when you need accurate, monotonic timing for benchmarks.
Practice Exercises
- Write a script
greet.jsthat reads a name fromprocess.argvand printsHello, <name>!. If the user also passes a second argument--loud, print the greeting in uppercase instead. (Hint: work withprocess.argv.slice(2).) - Write a script that requires a
DATABASE_URLenvironment variable. If it's missing, print an error toprocess.stderrand setprocess.exitCode = 1without throwing or callingprocess.exit(). Test it once with the variable set and once without. - Extend the graceful-shutdown server example: add a
setIntervalthat logsprocess.memoryUsage().rssevery 5 seconds, and make sure yourSIGINThandler callsclearIntervalbefore closing the server so nothing keeps the process alive after shutdown.
Summary
processis a globalEventEmitterNode provides automatically — no import needed — representing the currently running OS process.process.argvholds CLI arguments (real user arguments start at index 2);process.envholds environment variables, always as strings.- Prefer
process.exitCodeoverprocess.exit()so pending asynchronous work finishes before the process actually ends. process.nextTick()runs before Promise microtasks, which run before timers, which run beforesetImmediatecallbacks scheduled at the top level — but inside an I/O callback,setImmediatereliably beats a0mssetTimeout.- Always handle
unhandledRejectionanduncaughtException, and listen forSIGINT/SIGTERMto shut down cleanly instead of dropping in-flight work.
