process.loadEnvFile()

Parses a .env file and merges its variables into process.env.

Signature
process.loadEnvFile(path)
  • Added in Node.js v21.7.0

Parameters

ParameterTypeDescription
$pathoptionalstring | 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 raises ENOENT. Wrap optional files in try/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 __dirname or import.meta.dirname if that matters.
  • Everything is a string. PORT=8080 gives '8080', and DEBUG=false gives the truthy string 'false'.
  • Timing. Imports are hoisted, so a module that reads process.env at load time is evaluated before your loadEnvFile() call runs.
  • NODE_OPTIONS set inside a .env file has no effect.

Example

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

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.

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

The 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.

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

CLI 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.

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

  • Be the first to comment on this lesson.