Node.js Global Objects
In the browser, “global” usually means window. Node.js has no window — instead it exposes its own set of globals: some, like process, console, and Buffer, are available in every file without any import; others, like __dirname and require, only look global because Node quietly wraps every CommonJS file in a function that supplies them. Understanding which globals are real, which are per-module, and which only exist in one module system is essential to writing correct Node.js code and avoiding a whole class of “works in this file but not that one” bugs.
Overview / How it works
Node.js globals fall into three categories, and mixing them up is the source of most confusion:
1. True globals. These live on globalThis and are available in every file, in both CommonJS and ES modules, with no import: process, console, Buffer, the timer functions (setTimeout, setInterval, setImmediate and their clear* counterparts), queueMicrotask, and Web-standard APIs Node now ships built in such as fetch, URL, URLSearchParams, TextEncoder/TextDecoder, and structuredClone.
2. Module-scoped “fake globals” (CommonJS only). require, module, exports, __dirname, and __filename feel global because you never import them, but they don’t actually exist on globalThis. Node achieves this by wrapping every CommonJS file’s source in a function before running it, roughly: function(exports, require, module, __filename, __dirname) { /* your file's code */ }. That wrapper function is called once per file with those five arguments filled in, which is why each file gets its own __dirname and its own require, scoped to that file’s location. ES modules do not get this wrapper — that’s why __dirname and require throw a ReferenceError in a .mjs file or a package with "type": "module".
3. Runtime singletons. process represents the single running Node.js instance — its environment variables, command-line arguments, standard streams, and exit behavior. There is exactly one process object no matter how many modules you load, which makes it the natural place to read configuration and register shutdown handlers.
Under the hood, console.log writes synchronously to a TTY (like your terminal) but can write asynchronously when stdout is redirected to a file or pipe on some platforms — in practice this rarely matters, but it explains why extremely high-volume logging can occasionally lag behind other I/O. Buffer is Node’s fixed-length chunk of raw binary memory, sitting outside the V8 JavaScript heap; it existed before Uint8Array was fast in JavaScript engines and is still what streams and file/network APIs hand you by default.
Syntax
process.env.SOME_VAR
process.argv
process.exit(code)
__dirname // CommonJS only
__filename // CommonJS only
require(id) // CommonJS only
globalThis.myGlobal = value; // avoid — see Common Mistakes
| Global | Available in | Purpose |
|---|---|---|
process |
CJS & ESM | Env vars, argv, exit codes, platform info, streams |
console |
CJS & ESM | Logging to stdout/stderr |
Buffer |
CJS & ESM | Fixed-length binary data |
setTimeout/setInterval/setImmediate |
CJS & ESM | Scheduling callbacks |
queueMicrotask |
CJS & ESM | Schedule a microtask directly |
fetch, URL, structuredClone |
CJS & ESM (Node 18+) | Web-standard APIs built into Node |
__dirname, __filename |
CommonJS only | Absolute path of the current file/folder |
require, module, exports |
CommonJS only | Loading and exporting modules |
Examples
Example 1: Reading process information
// process-info.js
console.log("Node.js version:", process.version);
console.log("Platform:", process.platform);
console.log("Script arguments:", process.argv.slice(2));
console.log("Working directory:", process.cwd());
console.log("PORT env var:", process.env.PORT ?? 3000);
process.on("exit", (code) => {
console.log(`Process is exiting with code ${code}`);
});
Output (running node process-info.js --verbose):
Node.js version: v22.9.0
Platform: linux
Script arguments: [ '--verbose' ]
Working directory: /home/user/project
PORT env var: 3000
Process is exiting with code 0
Note that process.argv[0] is the path to the node binary and process.argv[1] is the script path — real CLI arguments start at index 2. Also note process.env.PORT is always a string or undefined, never a number, which is why config code should coerce it explicitly when doing arithmetic.
Example 2: __dirname and reading a file relative to the module
// read-config.js (CommonJS)
const fs = require("node:fs/promises");
const path = require("node:path");
async function main() {
console.log("This file:", __filename);
console.log("This directory:", __dirname);
const configPath = path.join(__dirname, "config.json");
try {
const raw = await fs.readFile(configPath, "utf8");
console.log("Config:", JSON.parse(raw));
} catch (err) {
console.error("Could not read config.json:", err.message);
}
}
main();
Output (no config.json present):
This file: /home/user/project/read-config.js
This directory: /home/user/project
Could not read config.json: ENOENT: no such file or directory, open '/home/user/project/config.json'
Because __dirname is per-file, this pattern of joining it with path.join is the standard, portable way to locate files next to a module regardless of what directory the process was launched from.
Example 3: Scheduling order — nextTick, microtasks, and timers
// timing-order.js
const fs = require("node:fs");
console.log("start");
fs.readFile(__filename, () => {
// Scheduled from inside an I/O callback, order is deterministic:
setTimeout(() => console.log("setTimeout"), 0);
setImmediate(() => console.log("setImmediate"));
});
process.nextTick(() => console.log("process.nextTick"));
Promise.resolve().then(() => console.log("promise microtask"));
console.log("end");
Output:
start
end
process.nextTick
promise microtask
setImmediate
setTimeout
All synchronous code runs first. Then Node drains the process.nextTick queue completely, then the promise microtask queue, before moving into the event loop’s phases. Because the timer and immediate were both scheduled from inside the poll phase (the fs.readFile callback), setImmediate is guaranteed to fire before the setTimeout(fn, 0) — that ordering guarantee only holds when both are scheduled during an I/O cycle; at the very top level of a script, the order between them is not guaranteed and depends on timing.
How it works step by step
- Node loads your file and, if it’s CommonJS, wraps it in the module function, injecting
require,module,exports,__filename, and__dirnameas arguments. - Your top-level synchronous code executes immediately.
- Any
process.nextTick()callbacks queued during that run are drained fully before anything else. - Any pending promise microtasks (
.then,awaitcontinuations,queueMicrotask) run next, in order. - Control passes into libuv’s event loop phases: timers (
setTimeout/setInterval), pending callbacks, poll (I/O), check (setImmediate), and close callbacks — with the nextTick and microtask queues drained again between every phase transition. - The process stays alive as long as there is pending work (an open server, a timer, a pending I/O operation). It exits automatically once the event loop has nothing left to do, or immediately if you call
process.exit().
Common Mistakes
Mistake: using __dirname in an ES module.
// wrong-esm.mjs
import fs from "node:fs/promises";
const data = await fs.readFile(__dirname + "/config.json", "utf8");
console.log(data);
This throws ReferenceError: __dirname is not defined because ES modules never get the CommonJS wrapper. The fix is to use import.meta.dirname (Node 20.11+) or, for older versions, derive it from import.meta.url:
// fixed-esm.mjs
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
const dirname = import.meta.dirname ?? path.dirname(fileURLToPath(import.meta.url));
const data = await fs.readFile(path.join(dirname, "config.json"), "utf8");
console.log(data);
Mistake: assigning to the global object to “share” state.
// bad-global.js
globalThis.currentUser = { id: 1, name: "Ada" };
function handler() {
console.log(globalThis.currentUser.name); // works, but fragile
}
Writing to globalThis creates hidden coupling between unrelated files, breaks under concurrent requests (one request’s data can leak into another’s), and makes testing painful. Export the value from a module and require/import it explicitly instead.
Mistake: calling process.exit() to “stop” after an error, discarding pending work.
fs.writeFile("out.txt", data, (err) => {
if (err) {
console.error(err);
process.exit(1); // any other pending writes/logs may be lost
}
});
process.exit() terminates immediately, without waiting for stdout to flush or other pending asynchronous operations to finish. Prefer letting the process exit naturally by not creating more pending work, or setting process.exitCode = 1 and returning — Node will exit with that code once the event loop is empty.
Best Practices
- Treat
process.envvalues as strings — coerce explicitly (Number(process.env.PORT)) before doing arithmetic. - Use
import.meta.dirnamein ESM and__dirnamein CommonJS; never assume one works in the other module system. - Prefer
process.exitCode = 1overprocess.exit(1)so buffered output and cleanup handlers get a chance to run. - Never write your own state onto
globalThis— export values from modules instead. - Register an
process.on("uncaughtException", ...)orunhandledRejectionhandler only for logging/telemetry before a controlled restart, not as a substitute for real error handling. - Use
Buffer.from()rather than the deprecatednew Buffer()constructor. - Reach for Node’s built-in
fetch,URL, andstructuredCloneinstead of adding a dependency for things Node already ships.
Practice Exercises
- Write a script that prints
process.platform,process.version, and the number of CPU cores available (hint:node:os‘scpus()), formatted as a small report. - Write a CommonJS module that uses
__dirnameto build the absolute path to a sibling file calleddata.txt, then reads it withfs/promisesand logs its contents (create the file yourself first). - Convert the module from the previous exercise into an ES module (
.mjs) usingimport.meta.dirnameinstead of__dirname, and confirm it produces the same output.
Summary
- Node.js has real globals available everywhere (
process,console,Buffer, timers,fetch,URL) that live onglobalThis. require,module,exports,__dirname, and__filenameare not true globals — they’re per-file arguments injected by the CommonJS module wrapper, and don’t exist in ES modules.- In ESM, replace
__dirname/__filenamewithimport.meta.dirname(Node 20.11+) orfileURLToPath(import.meta.url). - Execution order is: synchronous code, then
process.nextTick, then promise microtasks, then the event loop’s timer/poll/check phases. - Avoid writing your own properties onto
globalThis; prefer explicit module exports for sharing state.
