Back to core workflows Fix dependency resolution Tune package metadata Validate before publishing

Fixing 'process is not defined' in Browser Bundles

ReferenceError: process is not defined means code written for Node.js — usually a check like process.env.NODE_ENV — has reached a browser, where no process global exists. Webpack 4 used to hide this by injecting a process polyfill into every bundle; webpack 5, Vite, esbuild and Rollup do not. The error surfaces when an application upgrades its bundler, or when it installs a library that assumes Node.js globals. This guide explains how bundlers replace process.env at build time, how to fix the error in an application, and how library authors should write environment checks so consumers never see it.

Exact symptoms and error messages

The error appears in the browser console, often on the first render:

Uncaught ReferenceError: process is not defined
    at node_modules/some-lib/dist/index.js (index.js:14:5)
    at __require (chunk-ABCD1234.js:10:50)
    at app.tsx:3:22

Variants name other Node.js globals that browsers lack:

Uncaught ReferenceError: global is not defined
Uncaught ReferenceError: Buffer is not defined
Uncaught ReferenceError: require is not defined

In Vite projects the dev server sometimes works while the production build fails, or the reverse, because development pre-bundling and the production Rollup build handle process.env references differently.

Root cause analysis

process is a Node.js global. Browser bundles have always relied on bundlers to deal with it, in one of two ways: replacement — rewriting the expression process.env.NODE_ENV into a string literal such as "production" at build time — or polyfilling — shipping a small process object in the bundle. Webpack 4 did both automatically. Webpack 5 removed automatic Node.js polyfills to shrink bundles, and Vite and esbuild never had them. Replacement still happens, but only for the exact expressions each bundler is configured to replace. Anything else — process.env.API_URL, process.browser, a bare typeof process check done wrongly, or const { env } = process — reaches the browser untouched and crashes. How bundlers treat Node.js-specific code is covered in Bundling and Build Tooling for Libraries.

What a bundler does with a process reference A process.env.NODE_ENV expression is replaced by a literal; other exact expressions are replaced only if defined; everything else ships as-is and crashes in the browser. Code references process at build time Replaced process.env.NODE_ENV -> "production" NODE_ENV Replaced only if defined via define / import.meta.env other env keys Shipped as-is ReferenceError at runtime destructuring, process.x
Only expressions the bundler is told to replace disappear; the rest reach the browser as live references to a missing global.

Destructuring is the subtle case. const { NODE_ENV } = process.env cannot be replaced by a textual rewrite, because the bundler matches the full expression process.env.NODE_ENV. The same applies to process.env[key] and to passing process.env into a function.

Resolution for applications

Vite replaces process.env.NODE_ENV automatically in dependencies. For your own code, prefer import.meta.env:

// Vite exposes only variables prefixed with VITE_
const apiUrl = import.meta.env.VITE_API_URL;
const isProd = import.meta.env.PROD;

To support a dependency that reads other process.env keys, define them explicitly:

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

export default defineConfig({
  define: {
    'process.env.API_URL': JSON.stringify(process.env.API_URL ?? ''),
    'process.env.DEBUG': JSON.stringify(''),
  },
});

Webpack 5 uses DefinePlugin, and sets process.env.NODE_ENV from mode automatically:

// webpack.config.js
const webpack = require('webpack');

module.exports = {
  mode: 'production',
  plugins: [
    new webpack.DefinePlugin({
      'process.env.API_URL': JSON.stringify(process.env.API_URL),
    }),
  ],
};

esbuild:

esbuild src/app.ts --bundle --define:process.env.NODE_ENV=\"production\" --outfile=dist/app.js

Avoid defining the whole object ('process.env': JSON.stringify(process.env)). It inlines every environment variable on the build machine — including secrets — into the bundle.

As a last resort, polyfill. If a dependency uses process in ways that cannot be replaced, add a minimal global before your app loads:

// src/polyfills.ts — import first in main.tsx
globalThis.process ??= { env: { NODE_ENV: import.meta.env.MODE } } as unknown as NodeJS.Process;

Treat a polyfill as a workaround and report the issue to the library.

Handling process across bundlers Compares webpack 4, webpack 5, Vite and esbuild on automatic NODE_ENV replacement, automatic process polyfill and how to define extra keys. webpack 4 webpack 5 Vite esbuild NODE_ENV replaced yes from mode yes with --define process polyfilled automatic no no no Define other keys DefinePlugin DefinePlugin define option -define flag App env convention process.env process.env import.meta.env define
Modern bundlers replace NODE_ENV but no longer polyfill process — every other key must be defined explicitly.

Guidance for library authors

If you publish a package that runs in browsers, write environment checks that survive every bundler:

  1. Use only the exact expression process.env.NODE_ENV, never destructured, and never with bracket access. Every bundler replaces that expression.
  2. Guard any other access so it is safe when process is missing:
const debug =
  typeof process !== 'undefined' && process.env?.MY_LIB_DEBUG === '1';
  1. Do not read configuration from process.env in browser code. Accept options through your API instead.
  2. Use conditional exports to ship separate browser and Node.js builds when the code paths genuinely differ — a browser condition in exports lets bundlers pick the right file, as described in Understanding package.json Fields.

A related problem is Node.js built-in modules such as fs and path imported in browser-bound code; that failure mode is covered in Fixing 'Could not resolve node: Builtins' in a Browser Bundle.

Server-side rendering and isomorphic code

Frameworks that render on the server and hydrate in the browser — Next.js, Remix, Nuxt, SvelteKit — run the same module in two environments. On the server process exists; in the browser it does not. Code that reads process.env.SOME_KEY at module top level therefore works during server rendering and crashes during hydration, which often shows up as a page that renders once and then goes blank.

Each framework has a convention for exposing configuration to the client: Next.js inlines variables prefixed with NEXT_PUBLIC_ into client bundles by replacing process.env.NEXT_PUBLIC_X expressions; Vite-based frameworks use import.meta.env.VITE_X; Nuxt uses runtime config. Variables without the prefix are deliberately left unreplaced in client code so secrets cannot leak. If client code needs a value, give it the public prefix; if it must stay secret, keep the code that uses it on the server.

The same split applies to library authors targeting frameworks. Reading configuration through process.env from browser-reachable code forces every consumer to configure replacements; accepting configuration through a provider, a function argument or a framework-specific adapter does not.

Secrets and the define option

The define mechanism performs textual replacement at build time, which makes it easy to leak secrets by accident. Anything you define ends up as a string literal in JavaScript that every visitor downloads. Two rules keep it safe. First, define only values that are meant to be public — API base URLs, feature flags, build identifiers — never tokens or private keys. Second, define individual keys rather than whole objects, so a new secret added to the build environment cannot slip into the bundle unnoticed. A CI step that greps the production bundle for known secret patterns, or for the names of sensitive environment variables, is a cheap additional safeguard.

Worked example: an analytics SDK after a Vite migration

An application moves from Create React App (webpack 4 underneath, with automatic polyfills) to Vite. The build succeeds but the production page is blank with process is not defined in analytics-sdk/dist/index.js. The SDK contains const { NODE_ENV, ANALYTICS_HOST } = process.env;. Vite replaced nothing because of the destructuring. The short-term fix is a define for both keys in vite.config.ts; the long-term fix is an upstream change to read process.env.NODE_ENV directly and accept the host as an option. The team also adds a smoke test that loads the production build in a headless browser and fails on any uncaught error, which would have caught the problem before deploy.

Finding and fixing the offending reference Reproduce with a production build, locate the reference in the stack trace, classify it, then apply define, an upstream fix or a polyfill. vite build && preview reproduce in production mode read stack trace which file references process? define or polyfill unblock the release upstream fix exact NODE_ENV or guarded access
Fix the build with define first, then fix the source so the workaround can be removed.

Validation commands

# Find unreplaced process references in the production output
npx vite build && grep -rn "process\.env\|process\." dist/assets/*.js | head

# Serve the production build locally and watch the console
npx vite preview

# Check which dependency introduces the reference
grep -rln "process.env" node_modules/some-lib/dist/

Prevention and CI/CD guardrails

  • Smoke-test the production build in a headless browser and fail on uncaught errors.
  • Grep build output for process. as a cheap CI check in browser applications.
  • Lint library code with n/no-process-env or a custom rule that only allows process.env.NODE_ENV.
  • Never define the whole process.env object, which leaks build-machine secrets.

Frequently Asked Questions

Why did this start after upgrading to webpack 5? Webpack 5 stopped injecting Node.js polyfills automatically. Code that relied on the polyfilled process object now needs DefinePlugin replacements or an explicit polyfill.

Is it safe to define process.env as an empty object? Defining 'process.env': '{}' stops the crash for code that reads optional keys, but it also replaces process.env.NODE_ENV with undefined in some bundlers, which can enable development-only code paths. Define individual keys instead.

Why does it work in the Vite dev server but not the build? Dev-time pre-bundling with esbuild and the production Rollup build apply replacements at different stages. Always test the production build before assuming a fix works.

What about Buffer is not defined? The same principle applies: browsers have no Buffer. Prefer Uint8Array, TextEncoder and TextDecoder in code that runs in browsers. If a dependency requires Buffer, add the buffer package and assign globalThis.Buffer in a polyfill file loaded before the application, and ask the maintainer for a browser build.

Does TypeScript catch these references? Only if your browser code does not include @types/node. When Node.js types are loaded globally — common in monorepos with shared configuration — process type-checks everywhere. Give browser packages a types list that excludes node so the compiler flags Node.js globals in browser code.

Related

Bundling and Build Tooling for Libraries