process.loadEnvFile()
Parses a .env file and merges its variables into process.env.
process.loadEnvFile(path)- Added in Node.js v21.7.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$pathoptional | string | URL | Buffer | undefined= './.env' | Path to the .env file to load. Relative paths resolve against the current working directory, not the calling file. |
Return value
undefinedNothing. The call is synchronous and its effect is the mutation of process.env.
Errors
Throws an ENOENT Error if the file does not exist, and a SyntaxError-style failure if the file cannot be parsed.
Changelog
- PHP v20.12.0Backported to the v20 line alongside the v21.7.0 release.
- PHP v24.10.0No longer experimental (also in v22.21.0).
process.loadEnvFile() reads a .env file and copies its keys into process.env. It is Node.js's built-in answer to the dotenv package, with no dependency to install.
When to reach for it
Reach for it when the file must be loaded conditionally or from a computed path — per-environment configs, a fallback chain, a test fixture. When you simply want the default file loaded before your code runs, the CLI flag node --env-file=.env app.js is better, because it applies before the first module is evaluated.
What trips people up
- It throws if the file is missing. Unlike
dotenv, which returns an error object, a missing file raisesENOENT. Wrap optional files intry/catch. - Relative paths resolve against
process.cwd(), not the file calling it. Running the same script from a different directory changes what it loads. Build an absolute path from__dirnameorimport.meta.dirnameif that matters. - Everything is a string.
PORT=8080gives'8080', andDEBUG=falsegives the truthy string'false'. - Timing. Imports are hoisted, so a module that reads
process.envat load time is evaluated before yourloadEnvFile()call runs. NODE_OPTIONSset inside a.envfile has no effect.
Example
When to use it
- Layer a .env.local over a committed .env without adding a dependency.
- Load a fixture env file inside a test before constructing the config object.
- Pick a config file at runtime based on which deployment tier the script was started for.
Frequently asked questions
Do I still need the dotenv package in Node.js?
Not for basic loading: process.loadEnvFile() and the --env-file flag cover reading a .env file into process.env with no dependency. dotenv is still worth it if you rely on variable expansion, multiline value handling or its plugin ecosystem.
Why does process.loadEnvFile throw ENOENT?
Because it treats a missing file as an error rather than a no-op, unlike dotenv. Wrap optional files in try/catch, or start the process with --env-file-if-exists=.env.local, which ignores an absent file.
Does process.loadEnvFile override existing environment variables?
Yes — keys from the file are written into process.env and replace values already present, so a real shell variable can be silently overwritten. Snapshot the values you need beforehand if the shell must win.
More examples
A layered, optional loading chain
This reproduces the precedence chain frameworks give you, in fifteen lines and with no dependency. Two details matter: ENOENT is caught but parse errors are rethrown, so a typo in a committed .env still fails loudly; and path.join(__dirname, ...) makes the result independent of which directory the process was started from.
const path = require('node:path');
function loadIfPresent(file) {
// Resolve against this file, not the shell's working directory.
const abs = path.join(__dirname, file);
try {
process.loadEnvFile(abs);
return `loaded ${file}`;
} catch (err) {
if (err.code === 'ENOENT') return `skipped ${file} (absent)`;
throw err; // a parse error is a real bug - do not swallow it
}
}
// Later files override earlier ones.
console.log(loadIfPresent('.env'));
console.log(loadIfPresent(`.env.${process.env.NODE_ENV ?? 'development'}`));
console.log(loadIfPresent('.env.local'));
console.log('DATABASE_URL:', process.env.DATABASE_URL ?? '(unset)');
// Output on a machine with only .env and .env.local:
// loaded .env
// skipped .env.development (absent)
// loaded .env.local
// DATABASE_URL: postgres://localhost/app_devThe hoisting trap, and validating what you loaded
The commented block is the failure people hit first: an import statement runs before any statement in the file body, so a config module that snapshots process.env at load time sees nothing. A dynamic import after the load fixes the ordering. The validation block converts a would-be undefined-at-3am into an immediate, named failure with a non-zero exit status.
// WRONG - config.js is evaluated before loadEnvFile() runs,
// because ESM imports are hoisted to the top of the module.
// import { config } from './config.js';
// process.loadEnvFile('.env'); // too late
// RIGHT - load first, then import the module that reads process.env.
process.loadEnvFile('.env');
const { config } = await import('./config.js');
const required = ['DATABASE_URL', 'SESSION_SECRET', 'PORT'];
const missing = required.filter((k) => !process.env[k]);
if (missing.length > 0) {
console.error('missing required variables:', missing.join(', '));
process.exitCode = 1;
} else {
console.log('config ok, port', Number(process.env.PORT));
}
// Output when SESSION_SECRET is absent:
// missing required variables: SESSION_SECRETCLI flag versus API call
Prefer the flag whenever the filename is fixed, because it removes the hoisting problem entirely — the variables exist before the first line of your code runs. The API call earns its place only when the path depends on runtime input, as here, where the argv value selects the file.
// Flag form: applied before any user module is evaluated.
// $ node --env-file=.env app.js
// $ node --env-file=.env --env-file=.env.local app.js
//
// Tolerant variant that ignores a missing file (v22.9.0+):
// $ node --env-file-if-exists=.env.local app.js
// API form: use when the path is computed or the load is conditional.
const tier = process.argv[2] ?? 'staging';
process.loadEnvFile(`./config/${tier}.env`);
console.log(`tier=${tier} region=${process.env.AWS_REGION}`);
// $ node deploy.js production
// tier=production region=eu-west-1
Discussion