util.parseEnv()

Parses the text of a .env file into an object of string environment variables.

Signature
util.parseEnv(content)
  • Added in Node.js v21.7.0, v20.12.0

Parameters

ParameterTypeDescription
$contentstringThe 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

Try it yourself
Loading editor…
Press Run to execute the code.

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.

Example · javascript
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.

Example · javascript
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.

Example · javascript
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

  • Be the first to comment on this lesson.