util.parseEnv()
Parses the text of a .env file into an object of string environment variables.
util.parseEnv(content)- Added in Node.js v21.7.0, v20.12.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$content | string | The raw contents of a .env file. |
Return value
ObjectAn object whose keys are the variable names found in `content` and whose values are their strings.
Errors
Throws ERR_INVALID_ARG_TYPE if `content` is not a string. Malformed lines are skipped rather than raising an error.
Changelog
- PHP v24.10.0, v22.21.0This API is no longer experimental.
util.parseEnv() takes the text of a .env file and returns an object. It does no file reading and no assignment to process.env — you pass a string in, you get a plain object out.
When to reach for it
Use it when you need the values as data rather than as process state: validating a .env.example against a real .env in a CI check, merging several environment files with your own precedence rules, or building a config object without polluting process.env. If all you want is the variables loaded, Node has two better routes — the --env-file=.env CLI flag, and process.loadEnvFile() from node:process, which reads a file and assigns it for you.
Syntax it understands
# comments, blank lines, a leading export keyword, whitespace around =, single and double quoting, backtick quoting, and multi-line values inside quotes. Later duplicates win. Everything comes back as a string — PORT=8080 gives '8080', and DEBUG=false gives the truthy string 'false'. There is no variable expansion: URL=$HOST/api stays literal, unlike in dotenv-expand or a shell.
Example
When to use it
- Compare a project's .env against .env.example in CI to catch missing variables.
- Merge .env, .env.local and .env.production with precedence rules of your own.
- Build a validated config object from environment text without writing to process.env.
Frequently asked questions
Does util.parseEnv read a .env file?
No — it takes the file's contents as a string. Read the file yourself with `fs.readFileSync('.env', 'utf8')` and pass the result, or use `process.loadEnvFile('.env')`, which reads the file and assigns the variables to `process.env` in one step.
Do I still need the dotenv package in modern Node?
Not for basic loading. `node --env-file=.env` covers the common case, `process.loadEnvFile()` covers conditional loading, and `util.parseEnv()` gives you the parsed object. `dotenv` is still needed for `$VAR` expansion (via `dotenv-expand`), encrypted vaults, and Node versions before v20.12.0.
Why is my boolean environment variable always true?
Because `util.parseEnv()` returns every value as a string, and the non-empty string `'false'` is truthy. Compare explicitly with `env.FLAG === 'true'`, and use `Number(env.PORT)` for numbers — no type coercion happens during parsing.
More examples
The three ways to load a .env file, and when to use each
The flag is right for applications, `process.loadEnvFile()` for a script that must load lazily or conditionally, and `parseEnv()` only when the values are data you intend to inspect, validate or combine. The merge on the last lines is the case the other two cannot express: object spread makes precedence explicit, whereas both other mechanisms refuse to overwrite a variable that is already set in the real environment.
const util = require('node:util');
const fs = require('node:fs');
const process = require('node:process');
// 1. The CLI flag — nothing to write, applied before your code runs.
// $ node --env-file=.env app.js
// $ node --env-file-if-exists=.env.local --env-file=.env app.js
// 2. process.loadEnvFile() — reads the file AND assigns to process.env.
// Added in v20.12.0 / v21.7.0.
process.loadEnvFile('.env'); // defaults to './.env'
console.log(process.env.PORT);
// 3. util.parseEnv() — you control reading and what happens next.
const env = util.parseEnv(fs.readFileSync('.env', 'utf8'));
console.log(env.PORT); // process.env is untouched
// Only (3) lets you merge files with your own precedence:
const read = (p) => (fs.existsSync(p) ? util.parseEnv(fs.readFileSync(p, 'utf8')) : {});
const merged = { ...read('.env'), ...read('.env.local') }; // .local wins
console.log(merged);Validating a .env against .env.example
A drifting `.env.example` is a standard cause of "works on my machine". Because `parseEnv()` returns the keys as data rather than assigning them, both files can be parsed in the same process and compared directly — impossible with `--env-file`, which merges into `process.env` and loses which file a variable came from. Running this as a postinstall or CI step catches the drift before the application does.
const util = require('node:util');
const fs = require('node:fs');
function checkEnv(examplePath, actualPath) {
const expected = util.parseEnv(fs.readFileSync(examplePath, 'utf8'));
const actual = util.parseEnv(fs.readFileSync(actualPath, 'utf8'));
const missing = Object.keys(expected).filter((k) => !(k in actual));
const extra = Object.keys(actual).filter((k) => !(k in expected));
const blank = Object.keys(expected).filter((k) => actual[k] === '');
if (missing.length) console.error('missing:', missing.sort());
if (blank.length) console.error('present but empty:', blank.sort());
if (extra.length) console.warn('undocumented:', extra.sort());
return missing.length === 0 && blank.length === 0;
}
if (!checkEnv('.env.example', '.env')) {
console.error('environment is incomplete');
process.exitCode = 1;
}Quoting, multi-line values, and the absence of expansion
The last line is the trap that reaches production: `DEBUG=false` parses to the string `'false'`, which is truthy, so `if (env.DEBUG)` enables debugging. Always compare explicitly (`env.DEBUG === 'true'`) or coerce. The `URL` line shows the other gap — `parseEnv()` performs no `$VAR` interpolation, so if your `.env` relies on `dotenv-expand` semantics the values arrive uninterpolated.
const util = require('node:util');
const env = util.parseEnv(`
# Comments and blank lines are ignored.
PLAIN=no quotes needed
DOUBLE="spaces preserved"
SINGLE='literal $NOT_EXPANDED'
BACKTICK=\`he said "hi"\`
HASH_INSIDE="value # not a comment"
# Quoted values may span lines.
PRIVATE_KEY="-----BEGIN KEY-----
abc123
-----END KEY-----"
# No variable expansion: this stays literal.
HOST=example.com
URL=$HOST/api
# Everything is a string.
PORT=8080
DEBUG=false
`);
console.log(env.SINGLE); // literal $NOT_EXPANDED
console.log(env.HASH_INSIDE); // value # not a comment
console.log(env.PRIVATE_KEY.split('\n').length); // 3
console.log(env.URL); // $HOST/api
console.log(typeof env.PORT, Boolean(env.DEBUG)); // string true
Discussion