dotenv silently fails more often than it throws an error. When your process.env variables are still undefined after calling dotenv.config(), one of these 7 issues is almost always the cause.
Fix 1: require('dotenv').config() Must Be Line 1
This is the most common cause. ES module import statements are hoisted — meaning they execute before any code you write. If you import other modules before dotenv loads, those modules will not see the env variables.
Broken — imports hoist above dotenv:
import { connectDB } from './db'; // ← runs first, before dotenv
import dotenv from 'dotenv';
dotenv.config(); // ❌ too late — db.js already importedFixed — dotenv as first import:
import 'dotenv/config'; // ✅ line 1 — executes first
import { connectDB } from './db'; // now has access to process.envFix 2: .env File Must Be in the Project Root
dotenv resolves the .env path relative to process.cwd() — the directory from which you run the node command. This is usually your project root.
Wrong — .env inside src/:
my-project/ ├── src/ │ ├── .env ❌ dotenv will not find this │ └── index.js └── package.json # Even if you run: node src/index.js # process.cwd() is still /my-project — not /my-project/src
Correct — .env in root:
my-project/ ├── .env ✅ root — matches process.cwd() ├── src/ │ └── index.js └── package.json
Fix 3: Use the path Option for Custom Location
If you need to keep your .env file in a non-standard location, use the path option to tell dotenv exactly where to look.
Use path option for custom .env location:
const path = require('path');
require('dotenv').config({
path: path.resolve(__dirname, '../config/.env') // ✅ absolute path
});
// Or relative to current file:
require('dotenv').config({
path: path.join(__dirname, '.env') // always resolves from this file's dir
});
// Multiple .env files (dotenv v16+):
require('dotenv').config({ path: ['.env.local', '.env'] });Always use path.resolve() or path.join() — never rely on bare relative strings with dotenv.
Fix 4: ESM / TypeScript — Use import Differently
CommonJS (require) and ES modules (import) need different dotenv loading patterns.
ES Module (type: "module" in package.json):
// Option A: side-effect import (recommended)
import 'dotenv/config'; // ✅ first line
// Option B: explicit config call
import { config } from 'dotenv';
config();
// Option C: CLI flag (no code changes needed)
// node --require dotenv/config src/server.jsTypeScript with ts-node:
// src/index.ts — line 1:
import 'dotenv/config';
// Or via CLI:
// ts-node -r dotenv/config src/index.ts
// package.json script:
{
"scripts": {
"dev": "ts-node -r dotenv/config src/index.ts"
}
}Fix 5: Check for Typos — Use dotenv Debug Mode
dotenv silently ignores missing files. Enable debug: true to see exactly what dotenv finds — and where it is looking.
Enable dotenv debug mode:
require('dotenv').config({ debug: true });
// Console output will show:
// [dotenv][DEBUG] Trying to load /app/.env
// [dotenv][DEBUG] Loaded /app/.env
// [dotenv][DEBUG] "DB_HOST" is already defined in process.env and will not be overwritten
// If file is missing:
// [dotenv][DEBUG] Failed to load /app/.env: ENOENT: no such file or directoryThe debug output tells you the exact path dotenv resolved — use this to confirm your file is where dotenv expects it.
Fix 6: .env.local vs .env — Next.js Priority Order
Next.js supports multiple .env files with a strict priority order. Understanding this prevents unexpected variable values.
Next.js .env file priority (highest to lowest):
# 1. Process environment (system/shell vars) — always wins # 2. .env.local — local overrides, gitignored, highest .env priority # 3. .env.development — loaded when NODE_ENV=development # 3. .env.production — loaded when NODE_ENV=production # 3. .env.test — loaded when NODE_ENV=test # 4. .env — base defaults, committed to git # Recommended usage: # .env → default values, safe to commit (no secrets) # .env.local → your local secrets (add to .gitignore) # .env.production → production-specific non-secret values
For plain Node.js with dotenv (not Next.js), none of this hierarchy applies — dotenv only loads the one file you specify.
Fix 7: Docker / CI — System Variables Override .env
By default, dotenv will not overwrite variables already set in process.env. In Docker or CI systems, variables are often injected by the platform — making dotenv appear to do nothing.
dotenv skips variables already in environment:
# Docker run command sets DB_URL:
# docker run -e DB_URL=prod-db.example.com my-app
# .env file:
# DB_URL=localhost
require('dotenv').config();
console.log(process.env.DB_URL); // "prod-db.example.com" — .env ignored!Force .env values with override: true:
require('dotenv').config({ override: true }); // ✅ .env wins over system vars
// Or conditionally — only override in development:
if (process.env.NODE_ENV !== 'production') {
require('dotenv').config({ override: true });
}Verify dotenv is Working
Use this debug script to quickly confirm dotenv is loading correctly before adding more code:
debug-env.js — run with: node debug-env.js
// debug-env.js
const result = require('dotenv').config({ debug: true });
if (result.error) {
console.error('dotenv ERROR:', result.error.message);
process.exit(1);
}
console.log('dotenv loaded successfully.');
console.log('Parsed variables:');
for (const [key, value] of Object.entries(result.parsed || {})) {
// Mask secrets — show only the first 4 chars
const masked = value.length > 4 ? value.slice(0, 4) + '****' : '****';
console.log(` ${key} = ${masked}`);
}
console.log('\nChecking specific variables:');
const required = ['DATABASE_URL', 'API_KEY', 'PORT'];
for (const key of required) {
const status = process.env[key] ? '✅' : '❌ MISSING';
console.log(` ${key}: ${status}`);
}Validate your JSON config files →
Paste your JSON config and validate it against a schema — free, no signup.
Frequently Asked Questions
Why is dotenv not loading my .env file?
Most likely: the call is not on line 1, the .env is not in the project root, or a system variable is shadowing it. Enable debug: true to diagnose.
How do I use dotenv with ES modules?
Use import 'dotenv/config' as your first import, or start node with --require dotenv/config.
How do I use dotenv with TypeScript?
Add import 'dotenv/config' as the first line in your TypeScript entry file. For ts-node, use ts-node -r dotenv/config src/index.ts.
What is the difference between .env and .env.local?
In Next.js, .env.local overrides .env and is gitignored. For plain Node.js with dotenv, only the file you specify is loaded — the hierarchy is a Next.js feature.
Why does dotenv not work in Docker or CI?
System-injected variables take precedence. Use dotenv.config({ override: true }) to force .env values to win over system variables.