env.dev

Vite Env Variable Undefined? import.meta.env Fixes

import.meta.env.VITE_X undefined? Usual causes: missing VITE_ prefix, process.env in the browser, stale dev server, dynamic key access, wrong .env path.

By env.dev Updated

When import.meta.env.VITE_API_URL logs undefined, your fetch hits undefined/users and the app 404s before it renders. The cause is almost always one of eight things, and seven of them are not bugs in Vite — they are the static-replacement model working exactly as designed. Vite reads .env files once at startup and inlines each VITE_ value as literal text at build time (confirmed current in Vite 8.1, June 2026), so a missing prefix, a stale dev server, or a dynamic key lookup all collapse to the same symptom. Run your file through the env validator to rule out syntax first, then work down this list.

TL;DR

  • Only VITE_-prefixed variables reach import.meta.env. Everything else is undefined in client code by design — that is the security boundary, not a bug.
  • In the browser, process.env does not exist. Use import.meta.env.VITE_X, never process.env.VITE_X.
  • .env is read once at startup. Edit it and the running dev server keeps the old value until you restart it.
  • Replacement is literal text substitution. import.meta.env[key] and import.meta?.env?.VITE_X defeat it and return undefined.
  • The .env file must sit in the project root next to vite.config.ts, not in src/.

Quick diagnosis: symptom → cause → fix

SymptomCauseFix
Variable always undefined, dev and prodMissing VITE_ prefixRename to VITE_* or set envPrefix
process.env.VITE_X is undefined in the browserprocess.env does not exist client-sideRead import.meta.env.VITE_X
Worked before, undefined after editing .envDev server cached the old valueStop and restart the dev server
Every variable is undefined.env is in src/, not rootMove it beside vite.config.ts
Works in dev, undefined in the production bundleDynamic import.meta.env[key] or optional chainingUse the literal dotted form
Undefined only after deployValue set on host dashboard after buildSet it before build, then rebuild
Undefined only in VitestDependency ships "type": "module"Inject via test.env or a setup file

1. Did you forget the VITE_ prefix?

Vite exposes only variables that start with VITE_ to client code. Everything else is loaded into the Node process but deliberately withheld from import.meta.env so a stray DB_PASSWORD cannot leak into the shipped bundle. This is the single most common cause of an undefined variable.

typescript
// .env
VITE_API_URL=https://api.example.com
API_KEY=sk_live_123            // loaded, but never exposed to client code

// src/api.ts
console.log(import.meta.env.VITE_API_URL); // "https://api.example.com"
console.log(import.meta.env.API_KEY);      // undefined — no VITE_ prefix

For the full picture of how the prefix and static replacement work, see the companion Vite environment variables guide.

2. Are you reading process.env instead of import.meta.env?

In a browser bundle there is no process object, so process.env.VITE_API_URL is undefined (and on older setups throws ReferenceError: process is not defined). Vite uses import.meta.env precisely so it can statically replace the reference at build time — there is no runtime process.env to read. Developers migrating from Create React App or Webpack hit this constantly.

typescript
// WRONG — process.env is undefined in the browser with Vite
const url = process.env.VITE_API_URL; // undefined (or ReferenceError)

// CORRECT
const url = import.meta.env.VITE_API_URL;

Server-side code in an SSR framework (TanStack Start, Nuxt, SvelteKit) is the exception: there process.env is real and readable at runtime, the same way it is in plain Node.js. The rule only bites in code that runs in the browser.

3. Did you restart the dev server after editing .env?

Vite reads .env files once, when the server boots. It hot-reloads your source, not your environment, so a variable you added five seconds ago stays undefined until you stop the process and start it again. There is no watch-and-reload for .env by default.

bash
# After editing any .env file:
# Ctrl+C to stop, then start again
npm run dev

4. Is the .env file in the project root?

Vite loads .env files from the project root — the directory that holds vite.config.ts and package.json — not from src/. Putting the file under src/ is the exact mistake in Vite discussion #15525, where every variable read as undefined until the file moved up one level.

bash
my-app/
├── vite.config.ts
├── package.json
├── .env            # HERE — project root
└── src/
    ├── .env        # NOT here — Vite never reads this
    └── main.ts

If your .env genuinely lives elsewhere (a monorepo package, a shared config dir), point Vite at it with the envDir option in vite.config.ts rather than moving the file.

5. Are you accessing the key dynamically or with optional chaining?

Replacement is literal text substitution, not a runtime object lookup. Vite rewrites the exact string import.meta.env.VITE_API_URL into the inlined value; anything it cannot match as a static reference is left alone and evaluates to undefined in the build. The official docs state it plainly: dynamic key access like import.meta.env[key] will not work. The trap is that dynamic access often does work in dev (where import.meta.env is a real object) and only breaks in production — Vite 6+ module runners now throw Dynamic access of "import.meta.env" is not supported to surface it earlier.

typescript
// All of these break static replacement → undefined in the production bundle
const key = 'VITE_API_URL';
import.meta.env[key];                 // dynamic key
import.meta?.env?.VITE_API_URL;       // optional chaining (see discussion #15525)
const { VITE_API_URL } = import.meta.env; // destructuring before replacement

// CORRECT — the literal dotted form is what Vite rewrites
import.meta.env.VITE_API_URL;

// Need dynamic lookup? Build a map of literal references first:
const env = { apiUrl: import.meta.env.VITE_API_URL, cdn: import.meta.env.VITE_CDN };
env[someKey];

6. Did you edit the right .env file for the mode?

vite runs in development mode and loads .env.development; vite build runs in production mode and loads .env.production. A value you put in .env.development is simply not present when you build. More specific files win, and a variable already set in the shell or CI beats all of them:

FileLoaded whenPriority
.env.[mode].localMatching mode only, git-ignoredHighest file
.env.[mode]Matching mode onlyHigh
.env.localAlways, git-ignoredMedium
.envAlwaysLowest
bash
# Reproduce what the build actually sees
vite build --mode production   # loads .env.production
vite build --mode staging      # loads .env.staging (production-grade output)

7. Did you set the value after the build?

Because VITE_ values are inlined at vite build time, adding a variable to your hosting dashboard (Netlify, Vercel, Cloudflare Pages) does nothing for a bundle that was already built — the placeholder was resolved, or left undefined, when the artifact was produced. Set the variable in the build environment before the build step runs, then rebuild and redeploy. This is the build-time-versus-runtime split that catches every static front end.

If you truly need one artifact to serve many environments, do not bake config in at all — see build-time vs runtime env variables for the runtime-injection pattern.

8. Is TypeScript typing it as any?

A variable that compiles and runs fine can still look broken in the editor: without an ImportMetaEnv augmentation, every custom key is typed as any, and with strict settings TypeScript may flag the access. Declare your variables in src/vite-env.d.ts:

typescript
/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_API_URL: string;
  readonly VITE_SENTRY_DSN: string;
}

interface ImportMeta {
  readonly env: ImportMetaEnv;
}

One sharp edge from the official docs: the augmentation silently stops working if vite-env.d.ts contains a top-level import statement, because that turns the file from an ambient declaration into a module. Keep it import-free. Note the types promise a string, but Vite does not error on a missing variable — issue #8021 (still pending triage in 2026) asks for exactly that, so validate required variables at startup yourself.

Changing the prefix with envPrefix

If your variables use a different prefix (a shared PUBLIC_ convention, for instance), tell Vite with envPrefix. It defaults to VITE_ and accepts an array. Vite refuses to start if you set it to an empty string, because that would expose your entire environment — every CI token and shell secret — to the client bundle.

typescript
// vite.config.ts
import { defineConfig } from 'vite';

export default defineConfig({
  envPrefix: ['VITE_', 'PUBLIC_'], // now PUBLIC_FOO is also exposed
  // envPrefix: '' // Vite throws — would leak every secret
});

When undefined is correct, not a bug

  • Unprefixed variables in client code — withheld on purpose so secrets do not ship. If you see undefined for DATABASE_URL, the boundary is doing its job; move that read to the server.
  • A secret you wanted in the browser — there is no safe way. Every VITE_ value is plain text in DevTools. Proxy the call through a backend; the env vars security guide covers the threat model.
  • Undefined inside vite.config.ts — expected. The config runs in Node before .env is parsed; use loadEnv(mode, process.cwd(), '') instead of import.meta.env.
  • Undefined only in Vitest — a known edge case (vitest #2970): when a dependency declares "type": "module", Vitest may not inject import.meta.env into it. Define the value in test.env or a setup file rather than editing the dependency.

Quick debugging checklist

  1. Confirm the variable starts with VITE_
  2. Confirm you read import.meta.env.VITE_X, not process.env.VITE_X
  3. Restart the dev server after any .env edit
  4. Confirm .env is in the project root, not src/
  5. Use the literal dotted form — no [key], optional chaining, or destructuring
  6. Check the file matches the mode (vite build reads .env.production)
  7. For deploys, set the variable before the build runs, then rebuild

Frequently Asked Questions

Why is import.meta.env.VITE_X undefined?

Work down eight causes: the variable lacks the VITE_ prefix, you read process.env instead of import.meta.env, the dev server was not restarted after editing .env, the .env file sits in src/ instead of the project root, the key is accessed dynamically (import.meta.env[key]) or with optional chaining, the value is in the wrong .env.[mode] file, it was set on the host dashboard after the build, or TypeScript types it as any. The first six produce a true undefined at runtime.

Why does process.env.VITE_X not work in Vite?

There is no process object in the browser bundle, so process.env.VITE_X is undefined (or throws "process is not defined"). Vite exposes client-side variables on import.meta.env, which it statically replaces at build time. Use import.meta.env.VITE_X in any code that runs in the browser; process.env is only readable in server-side code under an SSR framework.

Why does my Vite variable work in dev but is undefined in production?

Almost always dynamic access. import.meta.env is a real object in dev, so import.meta.env[key], optional chaining, or destructuring all read fine there. In a production build Vite replaces only literal references like import.meta.env.VITE_X, so anything dynamic becomes undefined. Use the full static dotted form, or build a lookup map of literal references.

Where does the .env file go in a Vite project?

In the project root, next to vite.config.ts and package.json — not in src/. Placing it under src/ makes every variable read as undefined (Vite discussion #15525). If the file must live elsewhere, set the envDir option in vite.config.ts instead of moving it.

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

Yes. Vite reads .env files once at startup and does not watch them. Edits are invisible to the running server until you stop it and start it again. Vite hot-reloads source code, not environment variables.

Why is import.meta.env.VITE_X undefined only in my Vitest tests?

When a dependency package declares "type": "module" in its package.json, Vitest may fail to inject import.meta.env into that package (vitest issue #2970, closed as not planned). Rather than editing the dependency, define the value through Vitest config (test.env) or a setup file so the variable is present during the test run.

References

Rule out a malformed .env with the env validator, or read the .env file guide for quoting and syntax rules that silently break parsing.

Was this helpful?

Frequently Asked Questions

Why is import.meta.env.VITE_X undefined?

Work down eight causes: the variable lacks the VITE_ prefix, you read process.env instead of import.meta.env, the dev server was not restarted after editing .env, the .env file sits in src/ instead of the project root, the key is accessed dynamically (import.meta.env[key]) or with optional chaining, the value is in the wrong .env.[mode] file, it was set on the host dashboard after the build, or TypeScript types it as any. The first six produce a true undefined at runtime.

Why does process.env.VITE_X not work in Vite?

There is no process object in the browser bundle, so process.env.VITE_X is undefined (or throws "process is not defined"). Vite exposes client-side variables on import.meta.env, which it statically replaces at build time. Use import.meta.env.VITE_X in any code that runs in the browser; process.env is only readable in server-side code under an SSR framework.

Why does my Vite variable work in dev but is undefined in production?

Almost always dynamic access. import.meta.env is a real object in dev, so import.meta.env[key], optional chaining, or destructuring all read fine there. In a production build Vite replaces only literal references like import.meta.env.VITE_X, so anything dynamic becomes undefined. Use the full static dotted form, or build a lookup map of literal references.

Where does the .env file go in a Vite project?

In the project root, next to vite.config.ts and package.json — not in src/. Placing it under src/ makes every variable read as undefined (Vite discussion #15525). If the file must live elsewhere, set the envDir option in vite.config.ts instead of moving it.

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

Yes. Vite reads .env files once at startup and does not watch them. Edits are invisible to the running server until you stop it and start it again. Vite hot-reloads source code, not environment variables.

Why is import.meta.env.VITE_X undefined only in my Vitest tests?

When a dependency package declares "type": "module" in its package.json, Vitest may fail to inject import.meta.env into that package (vitest issue #2970, closed as not planned). Rather than editing the dependency, define the value through Vitest config (test.env) or a setup file so the variable is present during the test run.

Stay up to date

Get notified about new guides, tools, and cheatsheets.