process.env
Reads and writes the environment variables of the running Node.js process as a string-valued object.
process.env- Added in Node.js v0.1.27
Return value
ObjectAn object whose keys are environment variable names and whose values are always strings.
Errors
Assigning a value that cannot be implicitly converted to a string, such as a Symbol, throws a TypeError.
Changelog
- PHP v11.14.0Worker threads use a copy of the parent thread's process.env by default, configurable through the Worker constructor's env option.
- PHP v10.0.0Implicit conversion of an assigned value to a string is deprecated; a future version may throw when the value is not a string, number, or boolean.
process.env is the process's view of its environment: a plain object whose values are always strings. It is the standard way to pass configuration that differs between machines — database URLs, ports, API credentials, NODE_ENV — without baking it into source.
When to reach for it
- Anything that changes per deployment rather than per request.
- Secrets, which should never be committed to the repository.
- Feature switches you want to flip without a rebuild.
What trips people up
The string rule bites hard. process.env.DEBUG = 3 stores '3', and process.env.ENABLED = false stores the string 'false', which is truthy. Always coerce explicitly with Number() or an allow-list comparison.
Writes are local to this process. Setting a variable does not change the parent shell, and it does not reach a Worker thread unless you pass it through the env option. Node does not read a .env file on its own either — use node --env-file=.env app.js (v20.6.0+) or a loader library.
On Windows the main thread treats keys case-insensitively, so PATH and Path are the same variable; inside a Worker the copy is case-sensitive.
Reading the object goes through a native getter, so cache the values you need at startup rather than touching it inside hot loops.
Example
When to use it
- Read a database connection string that differs between local, staging and production.
- Switch logging verbosity with DEBUG or LOG_LEVEL without redeploying code.
- Fail fast at startup when a required secret is missing from the environment.
- Detect NODE_ENV to enable production-only caching or error pages.
Frequently asked questions
Why is my environment variable a string when I set it to a number?
Because assigning to process.env implicitly converts the value to a string, so `process.env.PORT = 3000` stores `'3000'`. Convert it back with `Number(process.env.PORT)` when you read it, and validate the result with `Number.isInteger()` since a typo produces `NaN` rather than an error.
Does Node.js read a .env file automatically?
No — process.env only contains variables the operating system passed to the process. Since v20.6.0 you can load a file with `node --env-file=.env app.js`, and older versions need a library such as dotenv called before any configuration is read.
Why doesn't setting process.env change my shell after the script exits?
Environment changes are local to the process and its future children; a child can never modify its parent's environment. `node -e 'process.env.foo = "bar"' && echo $foo` prints nothing for that reason — write the value to stdout and have the shell capture it instead.
More examples
Parsing typed configuration safely
The output shows the coercion boundary: `PORT=8080` arrives as the string `'8080'` and becomes a number only because `Number()` is applied, and `DEBUG=false` becomes `false` only because `bool()` compares against known strings. Writing `debug: Boolean(process.env.DEBUG)` instead would print `debug: true`, since any non-empty string is truthy.
function bool(value, fallback = false) {
if (value === undefined) return fallback;
return ['1', 'true', 'yes', 'on'].includes(value.toLowerCase());
}
const config = {
port: Number(process.env.PORT ?? 3000),
host: process.env.HOST ?? '0.0.0.0',
debug: bool(process.env.DEBUG),
maxSockets: Number(process.env.MAX_SOCKETS ?? 50),
};
if (!Number.isInteger(config.port) || config.port <= 0) {
throw new Error(`PORT must be a positive integer, got ${JSON.stringify(process.env.PORT)}`);
}
console.log(config);
// With DEBUG=false PORT=8080 in the environment:
// { port: 8080, host: '0.0.0.0', debug: false, maxSockets: 50 }Validating required variables at startup
The output shows a frozen snapshot taken at boot. Because the process exits with code 1 when a key is absent, a misconfigured deployment fails immediately with a readable message instead of throwing `Cannot read properties of undefined` deep inside a request handler an hour later. Run it with `node --env-file=.env app.js` to load the values from a file.
// Read the environment once, then never touch process.env again.
const env = Object.freeze({
NODE_ENV: process.env.NODE_ENV ?? 'development',
DATABASE_URL: process.env.DATABASE_URL,
SESSION_SECRET: process.env.SESSION_SECRET,
});
const missing = ['DATABASE_URL', 'SESSION_SECRET'].filter((key) => !env[key]);
if (missing.length > 0) {
console.error(`Missing required environment variables: ${missing.join(', ')}`);
process.exit(1);
}
console.log('mode:', env.NODE_ENV);
console.log('database host:', new URL(env.DATABASE_URL).hostname);
// mode: production
// database host: db.internalEnvironment inside Worker threads
The two output lines show the two behaviours: the first worker inherits a copy of the parent's environment as it stood when the worker was created, while the second sees only the object passed as `env`. Because each worker holds a copy, a later `process.env.SHARED = 'changed'` in the parent is invisible to both.
const { Worker, isMainThread } = require('node:worker_threads');
if (isMainThread) {
process.env.SHARED = 'set-by-parent';
// Default: the worker gets a copy of the parent's env.
new Worker(__filename);
// Explicit: only what you pass is visible.
new Worker(__filename, { env: { ROLE: 'isolated' } });
} else {
console.log('SHARED:', process.env.SHARED, '| ROLE:', process.env.ROLE);
}
// SHARED: set-by-parent | ROLE: undefined
// SHARED: undefined | ROLE: isolated
Discussion