Fix: process.env.VARIABLE_NAME Is Undefined (Node.js, React, Next.js, Vite)
Part of: React & Frontend Errors
Quick Answer
How to fix 'process.env.VARIABLE_NAME is undefined' and environment variables not loading from .env files in Node.js, React, Next.js, Vite, and Docker.
The Error
You try to access an environment variable and get undefined:
console.log(process.env.DATABASE_URL);
// undefinedOr your app crashes with a TypeError because you tried to use the value:
TypeError: Cannot read properties of undefined (reading 'split')In Next.js (browser-side code):
console.log(process.env.API_URL);
// undefined -- even though it's in your .env fileconsole.log(import.meta.env.API_URL);
// undefinedIn React (Create React App):
console.log(process.env.API_URL);
// undefinedAll of these mean the same thing: the environment variable you’re trying to read doesn’t exist in the current runtime context. The reason depends on your setup.
Why This Happens
Environment variables aren’t automatically available in your code just because you put them in a .env file. Something has to load them, and that “something” is different depending on your framework, your runtime, and how you start the process.
In plain Node.js, process.env is populated from the OS environment at process start. The OS environment includes whatever was set in your shell (export FOO=bar) plus what your parent process passed in. A .env file is just a text file on disk — the OS does not read it. Some library has to parse the file and inject its keys into process.env before your code reads them. That library is dotenv, or, since Node 20.6, the built-in --env-file flag.
In frontend frameworks (Next.js, Vite, CRA), the build tool reads .env at compile time and inlines specific variables into the bundle as string literals. This is why frontend frameworks require a prefix (NEXT_PUBLIC_, VITE_, REACT_APP_) — the prefix is the build tool’s allowlist. Variables without the prefix never make it into the browser bundle, so they appear as undefined on the client side regardless of what is in your .env file.
The most common causes:
- No
.envfile exists. You cloned a repo that has.envin.gitignorebut never created your own.envfile. - The
.envfile is in the wrong directory. Most tools expect it in the project root (next topackage.json). A.envfile insidesrc/or another subdirectory won’t be found. dotenvisn’t installed or configured. Plain Node.js and Express don’t read.envfiles by default. You need thedotenvpackage or Node 20.6+ with--env-file.- Missing framework-specific prefix. Next.js requires
NEXT_PUBLIC_, Vite requiresVITE_, and Create React App requiresREACT_APP_for variables exposed to the browser. - The dev server wasn’t restarted. Environment variables are loaded at startup. Changing
.envwhile the server is running has no effect until you restart. - In production/Docker, the variables were never set. A
.envfile on your local machine doesn’t magically appear in a Docker container or hosting platform.
A Note on the History of .env Conventions
The conventions for loading .env files have shifted significantly across tools and major versions. Knowing this history helps you understand why the same .env file behaves differently in different projects.
dotenv is the original library, released in 2013. The classic pattern is require('dotenv').config() at the top of your entry file. The library has stayed mostly stable, but dotenv v16 (October 2022) changed how multiline values are parsed: it now supports multiline values natively without escaping \n. Older code that wraps a multi-line PEM key in \n literals will now produce a string containing the literal \n characters instead of a real newline. If your private-key value started misbehaving after upgrading dotenv, this is why — switch to a true multi-line value with double quotes.
Node.js 20.6 (August 2023) added --env-file=.env as a built-in flag. This removed the need for dotenv as a dependency for new projects. Node 21 added --env-file-if-exists for optional files. Node 22 went further and made process.loadEnvFile() available as a runtime API. The built-in parser is intentionally a subset of dotenv’s behavior — it does not support variable expansion (FOO=${BAR}) or shell-style escaping. If you rely on those features, you still need dotenv even on modern Node.
Next.js introduced the NEXT_PUBLIC_ prefix in version 9.4 (May 2020). Before that, variables had to be referenced through next.config.js to be exposed to the client. The prefix-based approach was inspired by Create React App’s REACT_APP_ prefix, which dates back to 2017. The exact same idea — server-side variables stay server-side unless you opt them in — is now standard.
Vite uses import.meta.env instead of process.env because it ships ES modules to the browser, and the browser has no process global. Vite’s VITE_ prefix has been stable since the project began, but the loader picks up files in a specific order (.env, .env.local, .env.[mode], .env.[mode].local) that differs slightly from Next.js. Variables defined in shell environment always win over .env files.
The general rule across frameworks: if a variable is missing in the browser, you forgot the prefix. If it is missing on the server, the loader did not run. Those two failure modes have very different fixes.
Fix 1: Create the .env File with Correct Variable Names
If you don’t have a .env file, create one in your project root (the same directory as package.json):
DATABASE_URL=postgresql://localhost:5432/mydb
API_KEY=sk-abc123
PORT=3000Rules for .env files:
- No spaces around
=.API_KEY=valueis correct.API_KEY = valuewill include the spaces in the variable name or value depending on the parser. - No quotes needed for simple values.
API_KEY=sk-abc123works fine. If you must use quotes (for values with spaces), use double quotes:MESSAGE="hello world". - One variable per line. No semicolons, no commas.
- No
exportkeyword. WriteAPI_KEY=value, notexport API_KEY=value(unless you’re sourcing the file in a shell script, which is a different use case).
If you cloned a repo, look for a .env.example or .env.sample file. Copy it and fill in your values:
cp .env.example .envFix 2: Use the Right Prefix for Your Framework
Frontend frameworks intentionally limit which environment variables are exposed to the browser. This prevents accidentally leaking secrets like database passwords into client-side JavaScript.
Next.js: NEXT_PUBLIC_ prefix
Only variables starting with NEXT_PUBLIC_ are available in browser-side code:
# .env
NEXT_PUBLIC_API_URL=https://api.example.com
DATABASE_URL=postgresql://localhost:5432/mydb// Browser code (components, pages)
console.log(process.env.NEXT_PUBLIC_API_URL); // "https://api.example.com"
console.log(process.env.DATABASE_URL); // undefined (intentionally hidden)Variables without the prefix are still available in server-side code (API routes, getServerSideProps, Server Components, Route Handlers):
// app/api/users/route.ts (server-side)
console.log(process.env.DATABASE_URL); // works hereVite: VITE_ prefix
Vite uses import.meta.env instead of process.env. Only variables starting with VITE_ are exposed:
# .env
VITE_API_URL=https://api.example.com
SECRET_KEY=abc123console.log(import.meta.env.VITE_API_URL); // "https://api.example.com"
console.log(import.meta.env.SECRET_KEY); // undefinedNote: process.env does not exist in Vite projects by default. If you see process is not defined, switch to import.meta.env.
Create React App: REACT_APP_ prefix
CRA exposes variables starting with REACT_APP_:
# .env
REACT_APP_API_URL=https://api.example.com
SECRET_KEY=abc123console.log(process.env.REACT_APP_API_URL); // "https://api.example.com"
console.log(process.env.SECRET_KEY); // undefinedNote: Create React App is no longer actively maintained. If you’re starting a new project, consider Next.js, Vite, or Remix instead.
Fix 3: Install and Configure dotenv for Node.js / Express
Plain Node.js does not read .env files. You need the dotenv package.
Install it:
npm install dotenvThen load it at the very top of your entry file, before any other imports:
require('dotenv').config();
// Now process.env.DATABASE_URL is available
const express = require('express');If you’re using ES modules (import syntax):
import 'dotenv/config';
import express from 'express';The import order matters. If you import a module that reads process.env before dotenv runs, the variable will be undefined in that module. Always import dotenv first.
Node.js 20.6+ built-in .env support
Node.js 20.6 and later can load .env files without dotenv:
node --env-file=.env app.jsThis loads the .env file before your code runs. No package needed. You can also specify multiple files:
node --env-file=.env --env-file=.env.local app.jsOn Node 21+, use --env-file-if-exists=.env.local to skip the file silently when it does not exist (helpful in CI where the local override may be missing). On Node 22+, you can also call process.loadEnvFile() from inside your code for dynamic loading.
The built-in parser does not support variable expansion (FOO=${BAR}) the way dotenv-expand does. If you rely on that, stick with the dotenv package.
Custom .env path
If your .env file isn’t in the project root, pass the path to dotenv:
require('dotenv').config({ path: './config/.env' });Or with the --env-file flag:
node --env-file=./config/.env app.jsCommon Mistake: In Next.js, forgetting the
NEXT_PUBLIC_prefix on variables you need in the browser. Variables without the prefix are only available in server-side code — this is intentional to prevent accidentally leaking database passwords and API secrets into client-side JavaScript.
Fix 4: Restart the Dev Server
Environment variables are read when your application starts. If you add or change a variable in .env while the dev server is running, the change won’t take effect until you restart.
Stop the server (Ctrl+C) and start it again:
npm run devThis applies to every framework: Next.js, Vite, CRA, Express, and anything else that reads .env at startup.
Note: Some tools like Vite do support hot-reloading .env changes in newer versions, but the safest approach is always to restart.
Fix 5: Set Environment Variables in Docker and Production
A .env file is a local development convenience. In production, environment variables must be set through your deployment platform.
Docker
Option 1: --env-file flag:
docker run --env-file .env my-appOption 2: -e flag for individual variables:
docker run -e DATABASE_URL=postgresql://db:5432/mydb my-appOption 3: env_file in Docker Compose:
# docker-compose.yml
services:
app:
build: .
env_file:
- .envOption 4: ENV instruction in Dockerfile (for non-sensitive defaults):
ENV NODE_ENV=production
ENV PORT=3000Warning: Never put secrets (API keys, database passwords) in a Dockerfile. The values are baked into the image and visible to anyone who can pull it. Also watch out for Docker permission issues when running containers.
Hosting platforms
Set environment variables through your hosting platform’s UI or CLI:
Vercel:
vercel env add DATABASE_URLOr set it in the Vercel dashboard under Settings > Environment Variables.
Railway, Render, Fly.io: Each has an environment variables section in the dashboard.
AWS (ECS, Lambda, Elastic Beanstalk): Use the respective service’s environment configuration, AWS Systems Manager Parameter Store, or AWS Secrets Manager.
The common mistake is deploying an app that relies on a .env file when the file isn’t included in the Docker image or deployment artifact (because it’s in .gitignore).
Edge Cases
.env.local vs .env load order
Next.js loads environment files in this order (later files override earlier ones):
.env(base defaults).env.local(local overrides, not committed to git).env.development/.env.production/.env.test(environment-specific).env.development.local/.env.production.local/.env.test.local(environment-specific, local overrides)
.env.local is not loaded in the test environment to ensure test consistency.
Vite follows a similar pattern:
.env.env.local.env.[mode](e.g.,.env.development).env.[mode].local
If a variable is defined in multiple files, the most specific file wins. If DATABASE_URL is in both .env and .env.local, the .env.local value is used.
.env is gitignored but no .env.example exists
If .env is in .gitignore (as it should be), other developers who clone the repo won’t have it. Create a .env.example file that lists all required variables without their actual values:
# .env.example
DATABASE_URL=
API_KEY=
NEXT_PUBLIC_API_URL=Commit this file to the repository. Add a note in your README telling developers to copy it:
cp .env.example .envVariables available on the server but not the client
In Next.js and similar frameworks, this is intentional. If you need a variable on the client, rename it with the framework’s prefix (NEXT_PUBLIC_, VITE_, etc.).
If you need a secret value on the client side (rare, and usually a design issue), fetch it from an API route instead of exposing it as an environment variable.
dotenv doesn’t override existing environment variables
By default, dotenv does not overwrite variables that already exist in the environment. If DATABASE_URL is already set in your shell, the .env file value is ignored.
To force overwriting:
require('dotenv').config({ override: true });This matters when you have conflicting values between your shell environment and your .env file.
Still Not Working?
Typos in variable names
Environment variable names are case-sensitive. process.env.Database_Url is not the same as process.env.DATABASE_URL. Double-check the exact name in your .env file matches what you’re reading in code.
Spaces around =
This is wrong:
API_KEY = sk-abc123Depending on the parser, this sets a variable named API_KEY (with a trailing space) to sk-abc123 (with a leading space), or it fails silently. Write it without spaces:
API_KEY=sk-abc123Quotes around values
Most .env parsers strip surrounding quotes:
# Both produce the string: hello world
MESSAGE="hello world"
MESSAGE='hello world'But some parsers (especially shell-based ones) behave differently. If you’re getting unexpected quote characters in your values, try removing the quotes. Only use them for values that contain spaces or special characters.
Multiline values
If your variable contains a newline (like an RSA private key), wrap it in double quotes and use \n:
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nMIIE...\n-----END RSA PRIVATE KEY-----"Or use the actual newline syntax supported by dotenv:
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
MIIE...
-----END RSA PRIVATE KEY-----"Warning: Behavior changed in dotenv v16. Before v16, \n inside a double-quoted value was kept as the literal two characters \ and n — your code had to call .replace(/\\n/g, '\n') to get real newlines. From v16 onward, \n is expanded to a real newline by the parser. If you upgraded dotenv and your PEM keys broke (or the BEGIN/END lines stopped matching), this is why.
.env file encoding
Your .env file should be UTF-8 without BOM. Some Windows editors add a BOM (byte order mark) to the beginning of the file, which can cause the first variable to be unreadable. If your first environment variable is always undefined but the rest work, re-save the file as UTF-8 without BOM.
Comments in .env files
Lines starting with # are treated as comments:
# This is a comment
API_KEY=sk-abc123But inline comments are tricky. Some parsers support them, others don’t:
API_KEY=sk-abc123 # this might breakTo be safe, put comments on their own line.
process.env values are always strings
Environment variables are always strings. If you expect a number or boolean, you need to convert:
// Wrong -- this is the string "3000", not the number 3000
const port = process.env.PORT;
// Correct
const port = parseInt(process.env.PORT, 10) || 3000;
// Wrong -- this is the string "true", not the boolean true
if (process.env.DEBUG) { /* always true if set to anything, even "false" */ }
// Correct
const debug = process.env.DEBUG === 'true';Shell environment is shadowing your .env
Both Node’s built-in loader and dotenv refuse to overwrite a variable that already exists in process.env. If you exported DATABASE_URL in ~/.bashrc years ago and forgot, your .env value will silently lose the contest. Inspect the live environment before your loader runs:
env | grep DATABASE_URLIf you see it set there, either unset DATABASE_URL for the current shell or call dotenv.config({ override: true }) to force the file’s value to win.
Bundler stripped the variable in production
Frontend bundlers replace process.env.XYZ with the literal value at build time. If the variable was undefined at build time, the bundler inlines the string undefined. Reading it later from runtime configuration does nothing — the code has already been compiled. Always set frontend env vars at build time, not at container start time. On Vercel, Netlify, and Cloudflare Pages, this means setting variables in the project dashboard, not in a runtime .env.
dotenv didn’t load because the path was wrong
If your script runs from a different working directory than the project root, dotenv looks in the wrong place. The library searches process.cwd() by default, not the directory of the calling file. Either set the working directory before running:
cd /path/to/project && node scripts/task.jsOr pass an absolute path:
require('dotenv').config({ path: require('path').resolve(__dirname, '../.env') });This is a frequent source of “works in dev, undefined in cron” bugs.
Related: If you’re getting module import errors in your Node.js app, see Fix: Error Cannot find module.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: React Hydration Error — Text Content Does Not Match
How to fix React hydration errors — server/client HTML mismatches, useEffect for client-only code, suppressHydrationWarning, dynamic content, and Next.js specific hydration issues.
Fix: Next.js Module not found: Can't resolve 'fs' (or 'path', 'crypto', 'net')
How to fix Next.js Module not found Can't resolve fs error caused by importing Node.js modules in client components, wrong server/client boundaries, and missing polyfills.
Fix: Next.js Image Optimization Errors – Invalid src, Missing Loader, or Unoptimized
How to fix Next.js Image component errors including 'Invalid src prop', 'hostname not configured', missing loader, and optimization failures in production.
Fix: React useEffect runs infinitely (infinite loop / maximum update depth exceeded)
How to fix useEffect infinite loops in React — covers missing dependency arrays, referential equality, useCallback, unconditional setState, data fetching cleanup, event listeners, useRef, previous value comparison, and the exhaustive-deps lint rule.