⚡ Fastest fix
process is a Node.js-only global — there is no browser equivalent, and your bundler has to deliberately substitute environment values at build time to make process.env.X readable client-side. If you're on Vite, the fix is almost always to switch to its own mechanism:
// ❌ process.env.API_URL (undefined bundler shim, throws)
// ✅ import.meta.env.VITE_API_URL (Vite's client-exposed env mechanism)
// .env: VITE_API_URL=https://api.example.com (must be VITE_-prefixed)
What you're seeing
console.log(process.env.API_URL);
// Uncaught ReferenceError: process is not defined
// at main.js:14
// or, thrown from deep inside a third-party bundle you didn't write:
// Uncaught ReferenceError: process is not defined
// at Object.<anonymous> (vendor.chunk.js:1:88213)
The stack trace's top frame tells you whether the offending reference is in your code or a dependency's bundled output. That distinction changes which fix applies — you can edit your own code freely, but a broken third-party bundle needs either an upgrade, a build-time substitution, or (rarely) a shim.
30-second triage
- Building with Vite? → Fix 1 (import.meta.env)
- Building with plain webpack (no framework)? → Fix 2 (DefinePlugin)
- Using Create React App or an older toolchain? → Fix 3 (REACT_APP_ prefix)
- The throw is deep inside a third-party dependency's bundle? → Fix 4 (upgrade or shim)
- It worked before a tooling upgrade and now doesn't? → Why this happens — webpack 5's polyfill removal
Fix 1 — Vite: use import.meta.env, not process.env
When: your project uses Vite (directly, or via SvelteKit/Astro/vanilla-Vite/most modern React or Vue starters).
// .env (repo root)
VITE_API_URL=https://api.example.com
VITE_FEATURE_FLAG=true
// anywhere in client code:
const apiUrl = import.meta.env.VITE_API_URL; // ✅
console.log(import.meta.env.MODE); // "development" | "production", built in
console.log(import.meta.env.DEV); // boolean, built in
Vite deliberately does not expose unprefixed environment variables to client code — only variables prefixed with VITE_ are inlined into the bundle. This is a security boundary, not an oversight: a server-side .env often holds real secrets (database URLs, API keys with write access), and Vite refuses to accidentally ship those to every visitor's browser just because a variable happens to share a name with something client code reads. If you need a value in the client, it must be explicitly opted in via the prefix.
Fix 2 — Plain webpack: DefinePlugin or EnvironmentPlugin
When: a hand-rolled webpack config with no framework layer on top.
// webpack.config.js
const webpack = require("webpack");
module.exports = {
plugins: [
new webpack.DefinePlugin({
"process.env.API_URL": JSON.stringify(process.env.API_URL),
}),
// or, shorthand for a known list of names:
new webpack.EnvironmentPlugin(["API_URL", "NODE_ENV"]),
],
};
DefinePlugin performs a textual find-and-replace at build time — it rewrites the literal source text process.env.API_URL into the string you supplied, before the bundle is ever executed in a browser. No real process object is created; only the exact expressions you've told it about get substituted. Reference an untransformed property (a typo, or a name you forgot to list) and you're back to a genuine ReferenceError, because nothing replaced it.
Fix 3 — Create React App and similar: the REACT_APP_ prefix convention
When: an older or CRA-based toolchain, where process.env.REACT_APP_X is the documented pattern.
// .env
REACT_APP_API_URL=https://api.example.com
// client code — this specific pattern IS pre-processed by CRA's build:
console.log(process.env.REACT_APP_API_URL); // ✅ substituted at build time
console.log(process.env.DATABASE_URL); // ❌ not prefixed — still throws
This is the same mechanism as Fix 2 under the hood — CRA's webpack config wires up a DefinePlugin-equivalent step that specifically recognizes the REACT_APP_ prefix and inlines those values. It is not a real process object; reading any other process property in CRA-built code throws exactly the same as it would in an unconfigured webpack project.
Fix 4 — the error is inside a third-party dependency's bundle
When: the stack trace's throwing frame is inside node_modules output or a vendor chunk, not your own source.
// Option A — check for a newer release; many libraries fixed this
// exact issue when migrating off Node-oriented assumptions:
npm view <package-name> versions --json
// Option B — last-resort shim, ONLY for libraries that merely
// feature-detect process (e.g. `if (typeof process !== 'undefined')`)
// rather than reading real values from it:
if (typeof window !== "undefined" && typeof window.process === "undefined") {
window.process = { env: {}, browser: true, version: "" };
}
Reach for the shim only after confirming the library doesn't actually depend on a real value inside process.env — a shim that returns an empty env object will silence the crash but silently break any logic that expected a real environment variable to be present. Check the library's own documentation or issue tracker first; a stale dependency that assumes a Node-only runtime is frequently already fixed in a newer major version, and upgrading is the more durable fix.
Why this happens: process was never a web-platform global
process is part of Node.js's runtime API, not part of the ECMAScript specification or any browser's Web API surface — it exists because Node needs to expose OS-level process information (environment variables, argv, stdio streams, the event loop's nextTick) to server-side JavaScript, and browsers have no equivalent concept to expose, nor any reason to: a webpage isn't an operating system process the way a Node script is. When code written for (or copy-pasted from) a Node context runs unmodified in a browser, the reference is simply undefined, exactly like referencing any other undeclared identifier.
The reason this used to "just work" for many projects is historical, not architectural: webpack up through version 4 shipped **automatic Node-core polyfilling** by default — it would silently inject browser-compatible stand-ins for Node modules like path, fs, crypto, and yes, a fake process object, so that libraries written with Node in mind wouldn't immediately break when bundled for the browser. Webpack 5 (2020) **removed this automatic polyfilling as a deliberate breaking change** — the maintainers judged that most projects didn't need it, that it was bloating every bundle with unused shims, and that leaving it in silently encouraged code that made false assumptions about its runtime. Any project that silently relied on the old behavior, and then upgraded webpack (directly, or transitively via a framework upgrade), can start seeing this exact error with no other code changes at all — which is why it's worth checking your build tool's changelog around major-version bumps, not just your own diff.
Bundler behavior at a glance
| Bundler / tool | What actually happens | Correct client-code pattern |
|---|---|---|
| Vite | No shim at all; opt-in prefix required | import.meta.env.VITE_X |
| webpack 5 (bare) | No automatic shim; explicit plugin required | DefinePlugin / EnvironmentPlugin |
| webpack 4 and earlier | Auto-polyfilled a fake process | worked "by accident"; removed in v5 |
| Create React App | Build-time substitution for the REACT_APP_ prefix only | process.env.REACT_APP_X |
| Next.js | Substitutes process.env.NEXT_PUBLIC_X for client code | process.env.NEXT_PUBLIC_X |
Plain <script> tag, no bundler | Nothing shims anything, ever | Inline a config object yourself, or fetch config from an endpoint |
A note on the security angle
If you're debugging this error while trying to figure out why a secret isn't reaching your client code, resist the urge to treat the missing value as purely a bug to route around. Every bundler discussed here draws an intentional line between what's safe to ship to every visitor's browser (a public API base URL, a feature flag) and what must stay server-side only (database credentials, private API keys, signing secrets). The moment you find yourself reaching for a shim or a broad DefinePlugin rule that forwards all of process.env into the client bundle, stop and ask whether that value should be public at all — and if it genuinely needs to be, use the bundler's explicit, prefixed mechanism for it rather than a blanket pass-through, so the next engineer who adds a real secret to the same .env file doesn't get it shipped to the browser by accident.
Debugging checklist
- ✓ Identify the bundler (Vite / webpack / CRA / Next.js / none) — the fix is bundler-specific
- ✓ Vite: switch to
import.meta.env.VITE_Xand prefix the variable in.env - ✓ Bare webpack: add a
DefinePlugin/EnvironmentPluginrule for each variable you actually read - ✓ Stack trace points into
node_modules/a vendor chunk? Check for a newer release before shimming - ✓ If shimming as a last resort, confirm the library only feature-detects
processrather than needing real values from it - ✓ Recently upgraded webpack/a framework? Check whether automatic Node-core polyfilling was removed
- ✓ Never forward the whole
process.envobject into a client bundle — opt in per-variable
Frequently Asked Questions
What does 'process is not defined' mean?
process is a global object that exists only in Node.js — it exposes environment variables, command-line arguments, and OS-level process information to server-side code. Browsers have no equivalent global, so any client-side code that references process.env, process.argv, or process.platform throws this ReferenceError unless a build tool has specifically shimmed it in.
Why does process.env.NODE_ENV sometimes work in the browser without error?
Older tooling (webpack via DefinePlugin, Create React App, Babel's env preset) specifically detects the literal expression process.env.NODE_ENV at build time and replaces it with a string constant before the code ever reaches the browser — so no real process object needs to exist at runtime. This only works for that exact expression pattern the tool recognizes; process.env.ANYTHING_ELSE or a dynamically-computed key falls straight through to a real reference error.
How do I fix this in Vite?
Vite does not shim process at all by default. Use import.meta.env.VITE_MY_VAR instead of process.env.MY_VAR, and prefix any variable you want exposed to client code with VITE_ in your .env file — Vite deliberately does not expose unprefixed variables to the client, as a security measure against accidentally leaking server secrets into the bundle.
How do I fix this in webpack without a framework?
Use webpack's DefinePlugin to replace specific process.env.X references at build time with string literals, or EnvironmentPlugin for a shorthand over a known list of variable names. Both work by textually substituting the expression during the build — they do not create a real process object, so accessing an untransformed process property still throws.
Why did this only appear after I upgraded my build tooling?
Older webpack versions (4 and earlier) automatically polyfilled Node core modules and globals, including a fake process object, for browser compatibility. Webpack 5 removed this automatic polyfilling entirely as a deliberate breaking change to reduce bundle bloat, so code that silently relied on the old shim now throws in the browser after an upgrade with no other code changes.
Is polyfilling process in the browser ever the right fix?
Rarely, and only as a last resort for a third-party library you can't modify that assumes process exists purely for feature-detection (checking process.browser or process.version) rather than reading real secrets. A minimal shim like window.process = { env: {} } can unblock such a library, but reach for the bundler's proper environment-variable mechanism first — a shim doesn't give you real, per-build variable values.
Does this ever indicate a real security problem, not just a build error?
It can. If you see this error while debugging code you expected to already be sending process.env.SOME_SECRET_KEY to the browser, the failure is actually good news — it means that secret never made it into the client bundle. Investigate whether the intended behavior was to expose a public, non-secret config value (in which case use the bundler's client-safe env mechanism) rather than assuming the fix is to make process exist.
More JavaScript & build errors
Browse the full reference for JavaScript, Node.js, and build-tool errors — exact message, cause, and fix.