Skip to content

Fix: process.env.VARIABLE_NAME Is Undefined (Node.js, React, Next.js, Vite)

FixDevs · (Updated: )

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);
// undefined

Or 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 file

In Vite:

console.log(import.meta.env.API_URL);
// undefined

In React (Create React App):

console.log(process.env.API_URL);
// undefined

All 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 .env file exists. You cloned a repo that has .env in .gitignore but never created your own .env file.
  • The .env file is in the wrong directory. Most tools expect it in the project root (next to package.json). A .env file inside src/ or another subdirectory won’t be found.
  • dotenv isn’t installed or configured. Plain Node.js and Express don’t read .env files by default. You need the dotenv package or Node 20.6+ with --env-file.
  • Missing framework-specific prefix. Next.js requires NEXT_PUBLIC_, Vite requires VITE_, and Create React App requires REACT_APP_ for variables exposed to the browser.
  • The dev server wasn’t restarted. Environment variables are loaded at startup. Changing .env while the server is running has no effect until you restart.
  • In production/Docker, the variables were never set. A .env file 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=3000

Rules for .env files:

  • No spaces around =. API_KEY=value is correct. API_KEY = value will include the spaces in the variable name or value depending on the parser.
  • No quotes needed for simple values. API_KEY=sk-abc123 works 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 export keyword. Write API_KEY=value, not export 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 .env

Fix 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 here

Vite: 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=abc123
console.log(import.meta.env.VITE_API_URL); // "https://api.example.com"
console.log(import.meta.env.SECRET_KEY);   // undefined

Note: 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=abc123
console.log(process.env.REACT_APP_API_URL); // "https://api.example.com"
console.log(process.env.SECRET_KEY);        // undefined

Note: 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 dotenv

Then 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.js

This 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.js

On 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.js

Common 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 dev

This 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-app

Option 2: -e flag for individual variables:

docker run -e DATABASE_URL=postgresql://db:5432/mydb my-app

Option 3: env_file in Docker Compose:

# docker-compose.yml
services:
  app:
    build: .
    env_file:
      - .env

Option 4: ENV instruction in Dockerfile (for non-sensitive defaults):

ENV NODE_ENV=production
ENV PORT=3000

Warning: 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_URL

Or 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):

  1. .env (base defaults)
  2. .env.local (local overrides, not committed to git)
  3. .env.development / .env.production / .env.test (environment-specific)
  4. .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:

  1. .env
  2. .env.local
  3. .env.[mode] (e.g., .env.development)
  4. .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 .env

Variables 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-abc123

Depending 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-abc123

Quotes 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-abc123

But inline comments are tricky. Some parsers support them, others don’t:

API_KEY=sk-abc123 # this might break

To 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_URL

If 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.js

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

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles