path, os & process
Every Node.js process runs inside an operating system with a specific filesystem layout, CPU count, memory budget, and environment. Three built-in modules — path, os, and the global process object — give you portable access to these details without adding dependencies.
The path module hides the difference between Windows backslashes and POSIX forward slashes. The os module exposes machine-level facts such as CPU count, total memory, and the temporary directory. The process object connects your code to the outside world through command-line arguments, environment variables, signals, and lifecycle events.
This guide covers real APIs, real code, and production concerns: handling paths safely across platforms, reading environment variables, parsing command-line flags, reacting to process signals, avoiding memory leaks in timers, and bridging the gap between CommonJS and ESM when you need __dirname in an ES module.
Hard-coding / or \\ separators is one of the most common causes of cross-platform bugs. Node.js path normalizes separators automatically and provides helpers for joining, resolving, parsing, and comparing paths.
join vs resolve
path.join concatenates path fragments using the platform separator. path.resolve returns an absolute path by processing segments from right to left and prepending the current working directory if the result is still relative.
| 1 | const path = require('path'); |
| 2 | |
| 3 | // join: relative concatenation with platform separators |
| 4 | const logs = path.join('var', 'log', 'app.log'); |
| 5 | // On POSIX: 'var/log/app.log' |
| 6 | // On Windows: 'var\log\app.log' |
| 7 | |
| 8 | // resolve: absolute path from CWD or an explicit base |
| 9 | const absolute = path.resolve('var', 'log', 'app.log'); |
| 10 | // /home/user/project/var/log/app.log |
| 11 | |
| 12 | // resolve ignores earlier segments once an absolute segment appears |
| 13 | path.resolve('/etc', 'app', 'config.json'); |
| 14 | // /etc/app/config.json |
| 15 | |
| 16 | path.resolve('src', '/opt', 'data'); |
| 17 | // /opt/data |
info
normalize and parse
User-supplied paths often contain redundant segments such as .., ., or double separators. path.normalize collapses those without touching the filesystem. path.parse decomposes a path into root, directory, base, name, and extension.
| 1 | const path = require('path'); |
| 2 | |
| 3 | const messy = './data//users/../orders/./invoice.pdf'; |
| 4 | console.log(path.normalize(messy)); |
| 5 | // 'data/orders/invoice.pdf' |
| 6 | |
| 7 | const parsed = path.parse('/var/www/html/index.html'); |
| 8 | console.log(parsed); |
| 9 | // { |
| 10 | // root: '/', |
| 11 | // dir: '/var/www/html', |
| 12 | // base: 'index.html', |
| 13 | // ext: '.html', |
| 14 | // name: 'index' |
| 15 | // } |
| 16 | |
| 17 | console.log(path.extname('archive.tar.gz')); |
| 18 | // '.gz' (only last extension) |
| 19 | |
| 20 | console.log(path.basename('/tmp/file.txt', '.txt')); |
| 21 | // 'file' |
| Method | Purpose | Example Output |
|---|---|---|
| path.join(...) | Concatenate path segments | a/b/c.txt |
| path.resolve(...) | Absolute path from segments | /project/a/b/c.txt |
| path.normalize(p) | Collapse ., .. and double separators | clean/path |
| path.parse(p) | Decompose into root/dir/base/ext/name | object |
| path.extname(p) | Last extension only | .txt |
| path.relative(from, to) | Relative path between two paths | ../b/file.js |
warning
Windows uses drive letters and backslashes; POSIX systems use a single root and forward slashes. Node.js defaults to the platform-specific implementation, but path.posix and path.win32 let you force one format. This is essential when generating URLs, tar archives, or container image layers from a Windows developer machine.
| 1 | const path = require('path'); |
| 2 | |
| 3 | // Force POSIX-style paths regardless of OS |
| 4 | const posix = path.posix.join('src', 'components', 'Button.tsx'); |
| 5 | // 'src/components/Button.tsx' |
| 6 | |
| 7 | // Force Windows-style paths |
| 8 | const win = path.win32.join('src', 'components', 'Button.tsx'); |
| 9 | // 'src\components\Button.tsx' |
| 10 | |
| 11 | // Prefer url.pathToFileURL for correctness on all platforms |
| 12 | const { pathToFileURL } = require('url'); |
| 13 | console.log(pathToFileURL('/etc/hosts').href); |
| 14 | // file:///etc/hosts |
best practice
The os module provides read-only information about the machine and operating system. It is useful for sizing worker pools, choosing cache directories, logging platform-specific diagnostics, and deciding whether to enable memory-intensive features.
| 1 | const os = require('os'); |
| 2 | |
| 3 | console.log(os.platform()); // 'linux' | 'darwin' | 'win32' |
| 4 | console.log(os.release()); // kernel version |
| 5 | console.log(os.arch()); // 'x64' | 'arm64' |
| 6 | console.log(os.cpus().length); // number of logical cores |
| 7 | |
| 8 | // Memory in bytes — useful for cache sizing |
| 9 | const total = os.totalmem(); |
| 10 | const free = os.freemem(); |
| 11 | console.log(`Used: ${((total - free) / 1024 / 1024 / 1024).toFixed(2)} GB / ${(total / 1024 / 1024 / 1024).toFixed(2)} GB`); |
| 12 | |
| 13 | // Well-known directories |
| 14 | console.log(os.homedir()); // /home/alice |
| 15 | console.log(os.tmpdir()); // /tmp |
| 16 | console.log(os.hostname()); // laptop-alice |
| API | Returns | Common Use |
|---|---|---|
| os.platform() | 'linux', 'darwin', 'win32', ... | Conditional platform logic |
| os.arch() | 'x64', 'arm64', ... | Binary selection, feature flags |
| os.cpus() | Array of CPU objects | Worker pool sizing |
| os.totalmem() | Bytes | Cache budget, memory warnings |
| os.freemem() | Bytes | Health checks, backpressure |
| os.homedir() | User home path | Config and credential storage |
| os.tmpdir() | Temp directory path | Upload buffers, extraction |
info
The global process object is an instance of EventEmitter that represents the current Node.js process. It exposes command-line arguments, environment variables, the current working directory, process IDs, and methods to terminate or inspect the process.
| 1 | // process basics |
| 2 | console.log(process.pid); // process ID |
| 3 | console.log(process.ppid); // parent process ID |
| 4 | console.log(process.cwd()); // current working directory |
| 5 | console.log(process.title); // process title (can be set) |
| 6 | console.log(process.version); // Node.js version string |
| 7 | console.log(process.versions); // versions of V8, libuv, zlib, etc. |
| 8 | |
| 9 | // argv[0] is node executable, argv[1] is the script |
| 10 | // argv[2+] are user arguments |
| 11 | console.log(process.argv); |
| 12 | // ['node', '/app/bin/cli.js', '--port', '3000'] |
| 13 | |
| 14 | // Environment variables are read from the OS environment |
| 15 | console.log(process.env.NODE_ENV); |
| 16 | console.log(process.env.PATH); |
Parsing argv
For simple scripts you can parse process.argv manually. For production CLIs, prefer libraries like commander, yargs, or Node.js 20's built-in util.parseArgs which supports boolean flags, string options, and multiple values.
| 1 | const { parseArgs } = require('util'); |
| 2 | |
| 3 | const options = { |
| 4 | port: { type: 'string', short: 'p', default: '3000' }, |
| 5 | verbose: { type: 'boolean', short: 'v', default: false }, |
| 6 | env: { type: 'string', multiple: true }, |
| 7 | }; |
| 8 | |
| 9 | const { values, positionals } = parseArgs({ args: process.argv.slice(2), options }); |
| 10 | |
| 11 | console.log(values.port); // '8080' |
| 12 | console.log(values.verbose); // true |
| 13 | console.log(values.env); // ['staging', 'debug'] |
| 14 | console.log(positionals); // remaining positional args |
best practice
Environment variables are the standard way to inject configuration into Node.js applications without changing code. They work across clouds, containers, and CI systems, and they keep secrets out of source control. Read them from process.env once at startup and validate them with a schema.
| 1 | // config.js — load and validate env once |
| 2 | const port = Number.parseInt(process.env.PORT || '3000', 10); |
| 3 | const nodeEnv = process.env.NODE_ENV || 'development'; |
| 4 | const databaseUrl = process.env.DATABASE_URL; |
| 5 | |
| 6 | if (!databaseUrl) { |
| 7 | console.error('DATABASE_URL is required'); |
| 8 | process.exit(1); |
| 9 | } |
| 10 | |
| 11 | if (Number.isNaN(port) || port < 1 || port > 65535) { |
| 12 | console.error('PORT must be a valid TCP port'); |
| 13 | process.exit(1); |
| 14 | } |
| 15 | |
| 16 | module.exports = { port, nodeEnv, databaseUrl }; |
Loading a .env file is convenient for local development, but it should never be used in production. Docker, Kubernetes, and cloud secret managers are safer places for secrets. When you do load a local file, use dotenv before any other imports so that the rest of your application sees the values immediately.
| 1 | // index.js — load .env first |
| 2 | require('dotenv').config(); |
| 3 | |
| 4 | const config = require('./config'); |
| 5 | const server = require('./server'); |
| 6 | |
| 7 | server.listen(config.port, () => { |
| 8 | console.log(`Listening on port ${config.port}`); |
| 9 | }); |
| 10 | |
| 11 | // package.json script |
| 12 | // "start": "node -r dotenv/config index.js" |
warning
A Node.js process exits when the event loop has no more work, or when process.exit(code) is called. Exit code 0 means success; any non-zero value indicates failure. Shell scripts, systemd, Kubernetes, and CI systems all interpret this code.
| 1 | // Bad: exit immediately while async work is pending |
| 2 | process.exit(1); |
| 3 | |
| 4 | // Good: set exit code and let the event loop drain |
| 5 | process.exitCode = 1; |
| 6 | |
| 7 | async function main() { |
| 8 | try { |
| 9 | await connectDatabase(); |
| 10 | await startServer(); |
| 11 | } catch (err) { |
| 12 | console.error('Startup failed:', err); |
| 13 | process.exitCode = 1; |
| 14 | } |
| 15 | } |
| 16 | |
| 17 | main(); |
| Exit Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught fatal exception or general failure |
| 3 | Internal JavaScript parse error |
| 4 | Internal JavaScript evaluation failure |
| 128 + n | Fatal error signal n (e.g., SIGKILL = 137) |
best practice
Operating systems communicate with processes through signals. Node.js can listen for SIGINT (Ctrl+C), SIGTERM (polite shutdown), and SIGUSR2 (user-defined). A well-behaved server closes its HTTP listener, drains in-flight requests, flushes logs, and then exits cleanly.
| 1 | const http = require('http'); |
| 2 | |
| 3 | const server = http.createServer((req, res) => { |
| 4 | res.end('ok'); |
| 5 | }); |
| 6 | |
| 7 | const connections = new Set(); |
| 8 | server.on('connection', (conn) => { |
| 9 | connections.add(conn); |
| 10 | conn.once('close', () => connections.delete(conn)); |
| 11 | }); |
| 12 | |
| 13 | server.listen(3000, () => console.log('Server running')); |
| 14 | |
| 15 | async function shutdown(signal) { |
| 16 | console.log(`Received ${signal}, shutting down gracefully...`); |
| 17 | |
| 18 | server.close((err) => { |
| 19 | if (err) console.error('Error closing server:', err); |
| 20 | }); |
| 21 | |
| 22 | for (const conn of connections) { |
| 23 | conn.destroy(); |
| 24 | } |
| 25 | |
| 26 | await closeDatabase(); |
| 27 | console.log('Cleanup complete'); |
| 28 | process.exit(0); |
| 29 | } |
| 30 | |
| 31 | process.on('SIGTERM', () => shutdown('SIGTERM')); |
| 32 | process.on('SIGINT', () => shutdown('SIGINT')); |
| 33 | |
| 34 | // Handle uncaught exceptions before crashing |
| 35 | process.on('uncaughtException', (err) => { |
| 36 | console.error('Uncaught exception:', err); |
| 37 | shutdown('uncaughtException').catch(() => process.exit(1)); |
| 38 | }); |
warning
Node.js provides several ways to measure time. Date.now() is wall-clock time in milliseconds. process.hrtime.bigint() returns a monotonic high-resolution timestamp in nanoseconds that is not affected by system clock changes. process.uptime() tells you how long the process has been alive.
| 1 | const start = process.hrtime.bigint(); |
| 2 | await expensiveOperation(); |
| 3 | const elapsedNs = process.hrtime.bigint() - start; |
| 4 | console.log(`Operation took ${Number(elapsedNs) / 1e6} ms`); |
| 5 | |
| 6 | // process.uptime() returns seconds since process start |
| 7 | setInterval(() => { |
| 8 | console.log(`Uptime: ${process.uptime().toFixed(2)}s`); |
| 9 | }, 5000); |
| 10 | |
| 11 | // Resource usage and memory snapshot |
| 12 | console.log(process.resourceUsage()); |
| 13 | console.log(process.memoryUsage()); |
| 14 | // { rss, heapTotal, heapUsed, external, arrayBuffers, ... } |
info
CommonJS modules have __dirname and __filename variables. ES modules do not. If you are using .mjs files or "type": "module" in package.json, you can recreate these values with import.meta.url and the url and path modules.
| 1 | import { fileURLToPath } from 'url'; |
| 2 | import { dirname, join } from 'path'; |
| 3 | |
| 4 | const __filename = fileURLToPath(import.meta.url); |
| 5 | const __dirname = dirname(__filename); |
| 6 | |
| 7 | // Now you can resolve relative assets |
| 8 | const templatePath = join(__dirname, '..', 'templates', 'email.html'); |
| 9 | console.log(templatePath); |
In Node.js 20.11 and later, you can use the built-in import.meta.dirname and import.meta.filename properties without any helper imports. This removes boilerplate and reduces the chance of path bugs.
| 1 | // Node.js 20.11+ |
| 2 | import { join } from 'path'; |
| 3 | |
| 4 | const templatePath = join(import.meta.dirname, '..', 'templates', 'email.html'); |
| 5 | console.log(import.meta.filename); // absolute path to current file |
| 6 | console.log(import.meta.dirname); // absolute path to current directory |
best practice
Small mistakes around paths, environment variables, and process lifecycle cause large production issues: container restarts, leaked file descriptors, security holes, and silent configuration drift. The following patterns help you avoid the most common pitfalls.
Validate all environment variables at startup
Fail fast if a required variable is missing. Do not sprinkle process.env.SOME_VAR reads throughout your codebase; centralize them in a single config module.
Never trust process.cwd()
The current working directory depends on where the process was started, not where the source file lives. Resolve project paths with __dirname or import.meta.dirname instead of assuming process.cwd().
Use path.posix for deterministic output
Tools that generate source maps, manifest files, or container layers should use path.posix.joinso that output is identical regardless of the developer's operating system.
| 1 | // Robust shutdown with a safety timeout |
| 2 | async function gracefulShutdown(server) { |
| 3 | const forceExit = setTimeout(() => { |
| 4 | console.error('Forced shutdown after timeout'); |
| 5 | process.exit(1); |
| 6 | }, 25000).unref(); |
| 7 | |
| 8 | await new Promise((resolve) => server.close(resolve)); |
| 9 | await closeDatabasePool(); |
| 10 | |
| 11 | clearTimeout(forceExit); |
| 12 | process.exit(0); |
| 13 | } |