process.env.X is undefined

Quick answer

process.env.X is undefined usually because dotenv loaded after the code that reads it, the .env file is in the wrong folder, or — in the browser — the variable lacks the required client prefix (VITE_, NEXT_PUBLIC_, REACT_APP_).

The symptom

You read a variable you are sure you set, and it comes back empty — often crashing a downstream call that expected a string:

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

// …which then fails elsewhere:
TypeError: Cannot read properties of undefined (reading 'connect')
// or
Error: connect ECONNREFUSED — because the URL was undefined

There is no single “undefined” exception — process.env.X simply returns undefined when the key is not set, and the failure surfaces wherever you use the value. The fix depends entirely on where the code runs.

Cause 1: dotenv loaded too late (Node)

In plain Node, process.env only contains variables from your .env file after dotenv has run. If a module that reads process.env is imported before config() executes, it sees undefined. Because import statements are hoisted and run top-to-bottom, this happens constantly with ESM.

// ❌ db.js reads process.env at import time, before config() runs
import { db } from './db.js';        // reads process.env.DATABASE_URL → undefined
import dotenv from 'dotenv';
dotenv.config();

// ✅ Load env FIRST. Best: preload so it is set before any import.
// package.json script:
//   "dev": "node --env-file=.env server.js"   (Node 20.6+, no dotenv needed)
// or, with dotenv, import the side-effect first:
import 'dotenv/config';
import { db } from './db.js';

Modern Node (20.6+) has a built-in --env-file flag that loads the file before any code runs, sidestepping the ordering problem entirely.

Cause 2: wrong .env location or name

dotenv reads .env from the current working directory — the folder you run node from, not necessarily where the file lives. Running from a subfolder, or a monorepo package, means the file is never found. Check three things:

For the exact quoting and syntax rules of the file itself, see the .env file format explained — a stray quote or space is a frequent reason a value loads but is wrong.

Cause 3: browser code needs a prefix

This is the big one for frontend developers. Client bundlers refuse to expose arbitrary environment variables to the browser — otherwise your server secrets would ship in the JavaScript. Only variables with a framework-specific prefix are inlined into client code; everything else is undefined in the browser by design.

// Vite — only VITE_* is exposed, and via import.meta.env (NOT process.env)
import.meta.env.VITE_API_URL;     // ✅
import.meta.env.API_URL;          // ❌ undefined (no VITE_ prefix)

// Next.js — only NEXT_PUBLIC_* reaches the browser
process.env.NEXT_PUBLIC_API_URL;  // ✅ in client components
process.env.API_URL;              // ✅ server only / ❌ undefined in browser

// Create React App — only REACT_APP_*
process.env.REACT_APP_API_URL;    // ✅
process.env.API_URL;              // ❌ undefined

Two rules to remember: use the right prefix, and read from the right object (Vite uses import.meta.env, not process.env). Never put a secret behind a public prefix — anything exposed to the browser is readable by anyone.

The CRA → Vite migration trap: developers moving from Create React App to Vite often rename the variable to VITE_API_URL but keep reading it as process.env.VITE_API_URL — which is undefined in Vite's browser code. You changed the prefix but not the object. In Vite you must read it as import.meta.env.VITE_API_URL. Getting one right and the other wrong is the single most common reason a freshly-migrated Vite app returns undefined for a variable that is clearly set.

Cause 4: works locally, undefined after deploy

Your .env is gitignored (correctly — see never commit your .env), so it never reaches the server. Production platforms do not read .env files automatically. You must set the variables in the host's own environment settings:

For client-prefixed variables (VITE_, NEXT_PUBLIC_), the values are inlined at build time — they must be present when the build runs, not just at runtime, or they bake in as undefined.

Cause 5: forgot to restart the dev server

Environment variables are read once, at process start. dotenv loads the file when the process boots; bundlers inline values during the build. Editing .env while the server is running changes nothing until you restart it — hot module reload does not re-read environment variables. After any .env change, stop and restart the dev server. In Next.js specifically, if a stale build baked in the old value, delete the .next cache folder and rebuild — a plain restart sometimes reuses the cached, inlined value.

process.env undefined in Docker

A container starts with a clean environment, so variables you have on your host are not visible inside it unless you pass them in explicitly. A .env file copied into the image is also useless unless something loads it. There are two reliable ways to get variables into a container:

# docker-compose.yml — declare them under environment:
services:
  app:
    environment:
      DATABASE_URL: postgres://user:pass@db:5432/app
    # …or point at a file:
    env_file:
      - .env

# docker run — pass a file or individual vars
docker run --env-file .env my-app
docker run -e DATABASE_URL=postgres://... my-app

Two gotchas specific to Docker: client-prefixed variables (VITE_, NEXT_PUBLIC_) are inlined at build time, so they must be passed as ARG/build args during docker build — setting them only at docker run is too late. And do not bake real secrets into the image with ENV; inject them at runtime so they are not committed into image layers.

Monorepos and workspace projects

Monorepos are a frequent source of this error because of the working-directory rule from Cause 2. The .env usually lives at the repo root, but you run a package from its own folder:

root/
  .env                 ← the file lives here
  apps/
    api/   (package.json, runs from here)
    web/

# You run:
cd apps/api && npm start
# dotenv looks in apps/api for .env → not found → process.env.X is undefined

Three ways to fix it: point dotenv at the root file explicitly with dotenv.config({ path: '../../.env' }); use a tool that walks up the tree, such as dotenv-flow or Turborepo/Nx env handling; or give each app its own .env in its folder. Whichever you choose, be consistent — mixing a root .env with per-app files is itself a common source of confusion about which value actually loaded.

Debugging checklist

Frequently Asked Questions

Why is process.env.MY_VAR undefined in Node.js?

The most common reason is that dotenv has not loaded the .env file before the code that reads the variable runs. require('dotenv').config() must execute before any module that uses process.env, and the .env file must be in the directory you run node from. Other causes are a typo in the variable name, the variable not being set in the deployment environment, or the .env file being gitignored and missing on the server.

Why is my environment variable undefined in the browser with Vite or React?

Client-side bundlers only expose variables with a specific prefix, for security. Vite exposes only VITE_-prefixed variables on import.meta.env; Create React App exposes only REACT_APP_-prefixed ones on process.env; Next.js exposes only NEXT_PUBLIC_-prefixed ones to the browser. An unprefixed variable is stripped from the client bundle and reads as undefined in browser code.

Why does process.env work locally but is undefined after deploying?

Your .env file is almost certainly gitignored (as it should be), so it never reaches the server. Production platforms do not read .env files automatically — you must set the variables in the host's environment settings (Vercel, Netlify, Docker, systemd, CI secrets). Set the same keys there that you have locally.

Do I need to restart the dev server after changing .env?

Yes. Environment variables are read once at process start. dotenv loads the file when the process boots, and bundlers inline the values at build time. After editing .env you must restart the dev server (and in Vite/Next, sometimes clear the cache) for the new values to take effect — hot reload does not pick them up.

Why is import.meta.env.VITE_X undefined?

Either the variable is missing the VITE_ prefix (Vite only exposes VITE_-prefixed keys to client code), the .env file is not in the project root, or you did not restart the dev server after adding it. Also note that in Vite you read client variables from import.meta.env, not process.envprocess.env is largely empty in the browser.

Why is process.env.NEXT_PUBLIC_X undefined in the browser but defined on the server?

Next.js inlines NEXT_PUBLIC_ variables into the client bundle at build time. If the variable was added after the build, or the build ran without it set, the value is baked in as undefined. Rebuild with the variable present. Non-prefixed variables are server-only by design and are always undefined in browser code.

Working with a JSON API?

Format and validate any JSON response in your browser — nothing is uploaded to a server.

JSON Formatter JSON Validator All Error References
About the author

Pasindu Ishan is a software developer based in Sri Lanka. He builds privacy-first developer tools at JSON Dev Tools.