When process.env.MY_VAR returns undefined, the cause is almost always one of nine things: the .env file is missing, dotenv is not loaded early enough, the file path is wrong, import order defeats configuration, production has no .env file, there is a syntax error in the file, Windows line endings corrupt values, an existing environment variable takes precedence, or a framework-specific convention was missed. This guide walks through each cause with a concrete fix. Use our env validator to catch syntax issues instantly, and see the full .env guide for syntax reference.
1. Does the .env file actually exist?
The most common cause is trivial: the file is not where you think it is. Dotenv libraries default to reading .env from the current working directory (process.cwd() in Node.js, os.getcwd() in Python), which is wherever you ran the command — not necessarily the project root.
# Verify the file exists from your project root
ls -la .env
# Print the working directory your app actually sees
node -e "console.log(process.cwd())"
python -c "import os; print(os.getcwd())"If you run your app from a subdirectory (common in monorepos), the file will not be found. Either move the file or pass an explicit path.
2. Is dotenv installed and imported early enough?
Dotenv does not ship with Node.js or Python — you must install and invoke it yourself. If the import is missing or happens after other modules read process.env, those modules will see undefined.
# Node.js
npm install dotenv
# Python
pip install python-dotenvIn Node.js, import dotenv at the very top of your entry file, before any other imports:
// index.ts — this MUST be the first import
import 'dotenv/config';
// Now other modules can safely read process.env
import { app } from './app.ts';In Python, call load_dotenv() before accessing os.environ:
from dotenv import load_dotenv
load_dotenv() # Must come before os.getenv() calls
import os
db_url = os.getenv("DATABASE_URL")3. Is the file path wrong?
Relative paths resolve against the working directory, not the file that calls dotenv.config(). In monorepos or when running from a different directory, use an absolute path:
import dotenv from 'dotenv';
import path from 'node:path';
// Resolve relative to this file, not cwd
dotenv.config({ path: path.resolve(__dirname, '../.env') });
// Or in ESM (no __dirname)
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
dotenv.config({ path: path.resolve(__dirname, '../.env') });In Python, the same principle applies:
from pathlib import Path
from dotenv import load_dotenv
env_path = Path(__file__).resolve().parent.parent / '.env'
load_dotenv(dotenv_path=env_path)4. Are modules importing before dotenv.config() runs?
ES module imports are hoisted. If you call dotenv.config() in the module body but import other modules in the same file, those imports execute first. The fix is to use the side-effect import style:
// WRONG — config() runs after db.ts is already imported
import dotenv from 'dotenv';
import { db } from './db.ts'; // db.ts reads process.env.DATABASE_URL → undefined
dotenv.config();
// CORRECT — side-effect import runs immediately
import 'dotenv/config'; // Runs first due to side-effect execution
import { db } from './db.ts'; // process.env.DATABASE_URL is now setAlternatively, use the --require or --import flag to preload dotenv before any application code:
# CommonJS
node --require dotenv/config src/index.js
# ESM (Node 20.6+)
node --import dotenv/config src/index.ts5. Why are variables undefined in production?
Production environments do not use .env files. The file should not be deployed and should be in .gitignore. In production, set real environment variables through your hosting platform:
# Heroku
heroku config:set DATABASE_URL=postgres://...
# Docker
docker run -e DATABASE_URL=postgres://... myapp
# Cloudflare Workers
wrangler secret put DATABASE_URL
# AWS ECS / Lambda — use Parameter Store or Secrets Manager
# Kubernetes — use ConfigMaps or SecretsIf you conditionally load dotenv only in development, make sure production has every variable your app expects. Validate at startup to catch missing variables before they cause runtime errors.
6. Are there syntax errors in the .env file?
The .env format is deceptively simple. These common mistakes silently break parsing:
Spaces around the equals sign
# WRONG — most parsers treat " value" as the value (with leading space) or error
DATABASE_HOST = localhost
# CORRECT
DATABASE_HOST=localhostMissing quotes around values with special characters
# WRONG — the # starts an inline comment, truncating the value
CALLBACK_URL=https://example.com/auth#callback
# CORRECT
CALLBACK_URL="https://example.com/auth#callback"UTF-8 BOM character
Some editors (especially on Windows) prepend an invisible BOM (U+FEFF) to the file. This corrupts the first variable name, making it unrecognizable.
# Detect BOM
file .env
# Output containing "BOM" means the file has one
# Remove BOM
sed -i '1s/^\xEF\xBB\xBF//' .envRun your file through the env validator to catch these issues automatically.
7. Are Windows line endings (CRLF) corrupting values?
If your .env file uses Windows line endings (\r\n), the trailing \r may be included in the value. This causes subtle bugs: process.env.DB_HOST would be "localhost\r" instead of "localhost", and connection strings silently fail.
# Detect CRLF
cat -A .env | head -5
# Lines ending in ^M$ have CRLF
# Convert to LF
sed -i 's/\r$//' .env
# Or use dos2unix
dos2unix .envPrevent this permanently by adding a .gitattributes rule:
# .gitattributes
.env* text eol=lf8. Is an existing environment variable taking precedence?
By default, dotenv does not overwrite variables that already exist in the environment. If DATABASE_URL is set in your shell, your .env file's DATABASE_URL will be ignored.
# Check if a variable is already set in your shell
echo $DATABASE_URL
printenv | grep DATABASE_URLIn Node.js, use the override option if you want the file to win:
import dotenv from 'dotenv';
// Force .env values to override existing env vars
dotenv.config({ override: true });In Python, load_dotenv behaves the same way:
from dotenv import load_dotenv
# Override existing environment variables
load_dotenv(override=True)9. What framework-specific gotchas should I watch for?
Node.js with dotenv-expand
If you use variable interpolation (${BASE_URL}/api), you need dotenv-expand in addition to dotenv:
import dotenv from 'dotenv';
import { expand } from 'dotenv-expand';
expand(dotenv.config());
// Now ${BASE_URL}/api resolves correctlyPython — load_dotenv path resolution
By default, load_dotenv() searches the current directory and walks up parent directories. If your project structure is deep, it may find the wrong file or none at all. Always pass an explicit path in complex projects:
from dotenv import load_dotenv, find_dotenv
# find_dotenv() walks up directories to locate .env
load_dotenv(find_dotenv())
# Or be explicit
load_dotenv('/app/.env')For deeper Python coverage (Django, Flask, FastAPI, virtualenv interactions), see the Python environment variables guide.
Docker — build time vs runtime
Environment variables set with ENV in a Dockerfile are baked into the image and available at runtime. Variables set with ARG are only available during build and are not present at runtime. A common mistake is using ARG for runtime config:
# WRONG — ARG is not available at runtime
ARG DATABASE_URL
RUN echo $DATABASE_URL # Works during build only
# CORRECT — use ENV for runtime, ARG for build-only values
ARG BUILD_HASH
ENV DATABASE_URL=postgres://db:5432/app
RUN echo "Build: $BUILD_HASH"In multi-stage builds, ARG and ENV values do not carry over between stages. You must redeclare them in each stage that needs them:
FROM node:20 AS builder
ARG NODE_ENV=production
RUN npm run build
FROM node:20-slim
# Must redeclare — the builder's ARG is not inherited
ENV NODE_ENV=production
COPY --from=builder /app/dist ./distQuick Debugging Checklist
- Confirm the
.envfile exists at the expected path - Confirm dotenv is installed and imported before any code that reads env vars
- Use
import 'dotenv/config'instead of callingdotenv.config()after other imports - Check for whitespace around
=, missing quotes, and BOM characters - Run
cat -A .envto reveal hidden\rcharacters - Run
printenv | grep VAR_NAMEto check for collisions with existing env vars - In production, set variables through your platform — never deploy a
.envfile
References
- motdotla/dotenv — official Node.js dotenv documentation
- motdotla/dotenv-expand — variable interpolation for .env files
- theskumar/python-dotenv — getting started
- Node.js CLI docs — built-in
--env-fileflag (Node 20.6+) - Docker reference —
ARGvsENVin Dockerfiles