Uncaught ReferenceError: process is not defined - 11 Causes and Fixes in Vite

Uncaught ReferenceError: process is not defined shows up the moment browser code evaluates the identifier process. It means exactly what it says: there is no global named process in a browser, and Vite — unlike webpack 4 and Create React App — does not invent one for you.

The error is almost always one of two things. Either your own code still uses Node-style process.env.SOMETHING, or a third-party dependency was written for Node and ended up in your client bundle untouched.

This page gives you the one-line fix first, then 11 distinct causes with runnable reproductions, a diagnostic checklist for the cases that match none of them, and the reason Vite made this a hard error instead of a silent shim. Close-up of hands coding on a laptop, focusing on programming productivity.

ErrorUncaught ReferenceError: process is not defined
Where it happensJavaScript in the browser - Vite, Vite + React, SvelteKit, Astro, Rollup, esbuild, or any bundler that does not shim Node globals. Appears at runtime in the browser console, not at build time.
What it meansBrowser code tried to read a global called process, which only exists in Node.js, and nothing in your build replaced it.

The Fast Fix

Stop reading process.env in client code. Vite exposes env vars on import.meta.env, and only those prefixed with VITE_.

// before - throws in the browser
const apiUrl = process.env.VITE_API_URL

// after
const apiUrl = import.meta.env.VITE_API_URL
# .env - the prefix is what makes it reachable from the client
VITE_API_URL=https://api.example.com

Restart the dev server after editing .env — Vite reads env files at startup.

If the process reference is inside node_modules and not your code, jump to cause 4.

What Is Actually Causing It

1. Node-style process.env left in client code after a CRA migration

Reproduce it

// src/api.js - migrated from Create React App
const BASE = process.env.REACT_APP_API_URL

export function getUser(id) {
  return fetch(`${BASE}/users/${id}`).then(r => r.json())
}
Uncaught ReferenceError: process is not defined
    at api.js:2:14

Why it happens — CRA ran your code through webpack with DefinePlugin, which textually replaced process.env.REACT_APP_* before the browser ever saw it. Vite performs no such replacement for arbitrary keys, so the identifier process survives into the bundle and the browser throws on evaluation.

The fix

// src/api.js
const BASE = import.meta.env.VITE_API_URL

export function getUser(id) {
  return fetch(`${BASE}/users/${id}`).then(r => r.json())
}
# .env
VITE_API_URL=https://api.example.com

Changed: process.env.REACT_APP_API_URL became import.meta.env.VITE_API_URL, and the env var was renamed to carry the VITE_ prefix.

Confirm it worked — Run grep -rn "process\.env" src/ — it should return nothing. Then reload the page and console.log(import.meta.env.VITE_API_URL); it must print your URL, not undefined.


2. Destructuring or spreading the whole process.env object

Reproduce it

// src/config.js
const { NODE_ENV, VITE_API_URL } = process.env

export const config = { NODE_ENV, VITE_API_URL, ...process.env }

Why it happens — Even if you added a define entry for a specific key like process.env.NODE_ENV, Vite's define is a textual replacement of that exact expression. A destructuring pattern or spread touches the bare process identifier itself, which nothing replaced, so it throws before any property is read.

The fix

// src/config.js
export const config = {
  NODE_ENV: import.meta.env.MODE,
  VITE_API_URL: import.meta.env.VITE_API_URL,
}

Changed: no object-level access at all. Each value is read as a separate static property of import.meta.env, which is what Vite can statically replace. import.meta.env.MODE is Vite's equivalent of NODE_ENV.

Confirm it worked — Build and preview with npx vite build && npx vite preview. Open the built chunk in the Network tab and search for process — the string should be gone from your own module.


3. define config written without JSON.stringify

Reproduce it

// vite.config.js
import { defineConfig } from 'vite'

export default defineConfig({
  define: {
    'process.env.API_URL': 'https://api.example.com', // injected raw
    'process.env.DEBUG': true,
  },
})
// resulting code in the bundle
const url = https://api.example.com

Why it happens — define values are substituted as raw JavaScript expressions, not as string literals. A bare URL becomes a syntax error, and a key you forgot to list leaves process untouched — so you still get Uncaught ReferenceError: process is not defined for every unlisted key.

The fix

// vite.config.js
import { defineConfig } from 'vite'

export default defineConfig({
  define: {
    'process.env.API_URL': JSON.stringify('https://api.example.com'),
    'process.env.DEBUG': JSON.stringify(true),
  },
})

Changed: every value is passed through JSON.stringify so it lands in the bundle as a valid literal. Note this only patches the exact keys you list — it does not create a process object.

Confirm it worked — After npx vite build, grep the output: grep -r 'api.example.com' dist/assets/ should show the value wrapped in quotes.


4. A Node-targeted npm package references process in the browser bundle

Reproduce it

// src/main.js
import { thing } from 'some-node-oriented-lib'
thing()
Uncaught ReferenceError: process is not defined
    at node_modules/some-node-oriented-lib/dist/index.js:12:9

Why it happens — The stack trace points inside node_modules, not your source. The library was compiled for Node and reads process.env or process.platform at module scope. Vite's dependency pre-bundler replaces process.env.NODE_ENV in deps, but it does not fabricate the rest of the process object.

The fix

// vite.config.js
import { defineConfig } from 'vite'

export default defineConfig({
  define: {
    'process.env': '{}',   // process.env.FOO -> ({}).FOO -> undefined
    global: 'globalThis',  // same class of problem for `global`
  },
})

If the library touches bare process (for example typeof process), textual replacement of process.env is not enough. Shim it before your entry module loads:

<!-- index.html, above the module script tag -->
<script>window.process = { env: {} }</script>

Changed: the browser now has something to resolve process to. Prefer a browser build of the library if one exists (check its package.json exports/browser fields) — a shim hides the real incompatibility.

Confirm it worked — Reload with the console open. The ReferenceError should be gone; if the library now throws a different error about a missing env value, you have confirmed the shim resolved the identifier and the real problem is configuration.


5. Env var missing the VITE_ prefix, so you reverted to process.env

Reproduce it

# .env
API_URL=https://api.example.com
// src/api.js
console.log(import.meta.env.API_URL) // undefined
const BASE = process.env.API_URL     // ReferenceError

Why it happens — Vite deliberately exposes only variables matching envPrefix (default VITE_) to client code, so a stray DATABASE_PASSWORD in .env never leaks into a public bundle. Seeing undefined, developers reach for process.env and hit the ReferenceError instead.

The fix

# .env
VITE_API_URL=https://api.example.com
// src/api.js
const BASE = import.meta.env.VITE_API_URL

Changed: the variable was renamed to carry the prefix. If renaming is impossible, widen the prefix explicitly and accept the exposure risk:

// vite.config.js
export default defineConfig({ envPrefix: ['VITE_', 'PUBLIC_'] })

Confirm it worked — Stop and restart the dev server, then console.log(import.meta.env) in the browser. Your variable must appear as a key in the printed object.


6. A Node-only module imported into client code

Reproduce it

// src/paths.js
import path from 'path'
import 'dotenv/config'

export const uploadDir = path.join(process.cwd(), 'uploads')

Why it happens — dotenv and similar packages read process.env as their first act, and path/fs are Node builtins. Vite externalizes builtins for the browser and warns in the console, but any process access inside the imported module still runs and throws.

The fix

// src/paths.js - browser-safe
export const uploadDir = import.meta.env.VITE_UPLOAD_DIR ?? '/uploads'

Changed: the Node imports are gone. Vite loads .env files itself, so dotenv is never needed in a Vite app; path joining for URLs belongs in plain string logic or on the server.

Confirm it worked — Search for Node builtins in client code: grep -rnE "from '(path|fs|os|crypto)'" src/. Also confirm the console no longer prints Module "path" has been externalized for browser compatibility.


More causes (5 remaining)

7. A linked workspace package skips dependency pre-bundling

Reproduce it

// package.json
{ "dependencies": { "@acme/ui": "workspace:*" } }
// packages/ui/src/log.js - shipped as source, not pre-bundled
if (process.env.NODE_ENV !== 'production') console.warn('dev build')

Why it happens — Vite's pre-bundler rewrites process.env.NODE_ENV inside node_modules dependencies. Linked monorepo packages and anything in optimizeDeps.exclude bypass that step, so their process references reach the browser verbatim.

The fix

// vite.config.js
export default defineConfig({
  optimizeDeps: {
    include: ['@acme/ui'], // force pre-bundling of the linked package
  },
  define: {
    'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV ?? 'development'),
  },
})

Changed: the linked package is pre-bundled, and process.env.NODE_ENV is defined as a belt-and-braces fallback. process.env on the right-hand side is fine — vite.config.js runs in Node.

Confirm it worked — Run npx vite --force to drop the pre-bundle cache, then check node_modules/.vite/deps/ contains an entry for @acme/ui.


8. A shared helper imported by both vite.config.ts and the app

Reproduce it

// src/shared/env.ts
export const isProd = process.env.NODE_ENV === 'production'
export const port = Number(process.env.PORT ?? 5173)
// vite.config.ts - works here
import { port } from './src/shared/env'
// src/App.tsx - throws here
import { isProd } from './shared/env'

Why it happens — The same file is evaluated in two hosts. In vite.config.ts it runs under Node, where process exists, so the file looks correct. Imported from a component, it runs in the browser, where it does not. One module, two runtimes, one global that only exists in one of them.

The fix

// src/shared/env.browser.ts - client only
export const isProd = import.meta.env.PROD

// src/shared/env.node.ts - config and server only
export const port = Number(process.env.PORT ?? 5173)

Changed: the module was split by runtime. import.meta.env.PROD is Vite's built-in boolean, so the client file has no Node dependency at all.

Confirm it worked — Grep each file's import sites: nothing under src/ should import env.node.ts, and vite.config.ts should not import env.browser.ts.


9. Works in vite dev, breaks only in build or preview

Reproduce it

// vite.config.js
export default defineConfig(({ command }) => ({
  define: command === 'serve'
    ? { 'process.env': '{}' }
    : {}, // production build loses the shim
}))

Why it happens — Mode- or command-conditional config means dev and production resolve different globals. Dev also serves unbundled ESM where a browser extension or an injected dev script may happen to define process, masking the problem until you build.

The fix

// vite.config.js
export default defineConfig({
  define: {
    'process.env': '{}', // applied to serve and build alike
  },
})

Changed: the define block is unconditional. Keep runtime shims identical across commands and put only genuinely dev-only concerns (proxies, sourcemaps) behind a command check.

Confirm it worked — Run npx vite build && npx vite preview and load the preview URL in a private window with extensions disabled. No ReferenceError means the production path is clean.


10. process used in an inline script in index.html or in a worker

Reproduce it

<!-- index.html -->
<script>
  window.__API__ = process.env.VITE_API_URL
</script>
<script type="module" src="/src/main.js"></script>

Why it happens — import.meta.env only exists inside modules that Vite transforms. A classic inline <script> in HTML is not an ES module, so neither process nor import.meta is available there.

The fix

<!-- index.html - Vite substitutes %VITE_*% during HTML processing -->
<script>
  window.__API__ = '%VITE_API_URL%'
</script>

For a worker, read the value in the module that spawns it and pass it in:

// src/main.js
const worker = new Worker(new URL('./worker.js', import.meta.url), { type: 'module' })
worker.postMessage({ apiUrl: import.meta.env.VITE_API_URL })

Changed: HTML uses Vite's %VITE_*% placeholder syntax, and the worker receives config by message instead of reading a global.

Confirm it worked — View source on the served page — the placeholder should already be replaced with the literal value, with no % signs left.


11. Dynamic key access defeats static replacement

Reproduce it

// src/flags.js
function flag(name) {
  return process.env[`VITE_FLAG_${name}`] // bare `process` -> ReferenceError
}

function viteFlag(name) {
  return import.meta.env[`VITE_FLAG_${name}`] // undefined after build
}

Why it happens — Both env mechanisms are compile-time text substitution, not runtime object lookups. A computed key cannot be matched by the replacer, so process is left bare and import.meta.env[key] is not replaced either — the second form fails silently in a production build even though it appears to work in dev.

The fix

// src/flags.js - one static read per variable, assembled into a map
const FLAGS = {
  BETA: import.meta.env.VITE_FLAG_BETA,
  DARK: import.meta.env.VITE_FLAG_DARK,
}

export function flag(name) {
  return FLAGS[name]
}

Changed: every env var is read as a literal static property, then looked up dynamically from a plain object the bundler can see.

Confirm it worked — Build, then grep the output for VITE_FLAG_BETA: you should find the substituted value and no occurrence of import.meta.env[.

Vibrant JavaScript code displayed on a screen, highlighting programming concepts and software development. ## None of Those? Narrow It Down
  1. Open the browser console and expand the stack trace to find the file that evaluated process. If the top frame is under src/, it is your code - go to causes 1, 2, 6, 8 or 11. If it is under node_modules/ or .vite/deps/, go to cause 4 or 7.
  2. Run grep -rn "process\." src/ (and your workspace packages). Any hit is a confirmed occurrence you must rewrite; zero hits rules out all of your own source and points at a dependency or at index.html.
  3. Run grep -n "process" index.html and check for classic inline scripts. A hit confirms cause 10, because import.meta.env is unavailable outside modules.
  4. Log import.meta.env in the failing module. If your variable is absent from the object, the env var is not prefixed or the dev server was not restarted - cause 5, and it is not a process problem at all.
  5. Compare npx vite against npx vite build && npx vite preview. Failing only in preview points at mode-conditional define config or a dev-only shim - cause 9.
  6. Reload in a private window with all browser extensions disabled. If the error only appears there, something in your normal browser profile was defining process and masking the real bug.
  7. Run npx vite --force to invalidate the pre-bundle cache. If the error appears or changes after this, the dependency graph was being served from a stale optimize step - cause 7.
  8. As a last narrowing step, add define: { 'process.env': '{}' } temporarily. If the ReferenceError becomes a different error about a missing value, you have proved the identifier was the only problem and can now fix the real source instead of keeping the shim.

Why This Error Exists At All

process is not part of the JavaScript language. Open the ECMAScript spec and you will find globalThis, JSON and Math, but no process — it is a host object injected by Node.js, in the same family as Buffer, __dirname and require. The browser is a different host with a different set of globals, and it has no obligation to provide Node's. When the engine hits an identifier that resolves nowhere in the scope chain, the spec's only answer is to throw a ReferenceError. That is why the message says not defined rather than undefined: the name has no binding anywhere, so evaluation cannot even begin.

The reason this feels like a regression is that the previous generation of tooling hid it. Webpack 4 and Create React App shipped automatic Node polyfills, so process.env.NODE_ENV and process.browser quietly worked in client code. That convenience cost real bytes and, worse, blurred the line between server and client — which is how secrets end up in public bundles. Vite reversed the default on purpose: no automatic Node shims, and env vars reach the client only behind an explicit VITE_ prefix. The ReferenceError is the designed-in signal that a piece of server-shaped code crossed into the browser.

The second half of the design is when substitution happens. import.meta.env.VITE_API_URL and Vite's define entries are not runtime lookups on an object — they are static text replacements performed at build time. The bundler scans for that exact expression and writes a literal in its place. This explains every confusing variant on this page at once: a destructuring pattern has no matching text, a computed key has no matching text, and an inline HTML script was never run through the transform in the first place. Once you see env access as compile-time find-and-replace rather than a global object, you can predict in advance which of these forms will survive a production build — and you will recognise Uncaught ReferenceError: require is not defined and global is not defined as the same error wearing a different name. Man intently working on computer programming with code displayed on dual monitors in a dimly lit room.

Stop It From Coming Back

  • Add an ESLint guard so the pattern cannot return: "no-restricted-globals": ["error", { "name": "process", "message": "Use import.meta.env in client code" }] in the config block that covers src/, with an override re-allowing it in vite.config.* and server directories.
  • Declare your env surface in src/vite-env.d.ts with an interface ImportMetaEnv { readonly VITE_API_URL: string } so TypeScript flags a typo or a missing variable at compile time instead of handing you undefined at runtime.
  • Validate env at startup with a schema — a small zod object parsed from explicit import.meta.env.VITE_* reads — so a misconfigured deployment fails loudly on boot rather than mid-session.
  • Run vite build && vite preview in CI and load the preview URL with Playwright, failing the job on any console error. This is the only check that catches shims which exist in dev but not in the production bundle.
  • Set envPrefix explicitly in vite.config.js and keep server-only secrets in files the client build never reads, so the prefix boundary stays a deliberate decision instead of a default nobody remembers.
  • Split any module touched by both vite.config.* and src/ into *.node.ts and *.browser.ts, and add a CI grep that fails if a file under src/ imports the Node variant.

Related Guides

Errors You Will Probably Hit Next

  • Uncaught ReferenceError: require is not defined
  • Uncaught ReferenceError: global is not defined
  • Module "path" has been externalized for browser compatibility. Cannot access "path.join" in client code.

ReferenceError means the name resolves nowhere, so read the stack frame first: your file is a rewrite job, node_modules is a config job. Treat import.meta.env as build-time find-and-replace, which is why only static, literal property reads survive a production build. And verify in vite preview, never in vite dev alone — dev is the environment most likely to lie to you about which globals exist.

댓글

이 블로그의 인기 게시물

TypeError: Cannot read properties of undefined (reading 'map') - 11 Causes and Fixes

psql: FATAL: password authentication failed for user - 10 Causes and Fixes

Error: ERR_MODULE_NOT_FOUND: Cannot find package - 10 Causes and Fixes