Production builds failing in a create-react-app (CRA) project is stressful because everything works in dev then npm run build (or CI) explodes. The good news: most CRA production failures fall into a small set of predictable categories.
Start With the Right Signal: Reproduce the Production Build
Before changing anything, reproduce the same build that’s failing in production:
# clean install (best for catching lockfile/CI differences)
rm -rf node_modules
npm ci# run a production build
npm run build
If your production uses Yarn or pnpm, match it locally. CI failures often come from dependency resolution differences across package managers.
Enable more logging:
# helpful for CI logs
CI=true npm run build
Node.js Version Mismatch (Most Common in CI)
CRA + tooling can break if production uses a different Node version than your laptop.
Symptoms:
error:0308010C:digital envelope routines::unsupported(OpenSSL / Node 17+)- Random Babel/Webpack errors only in CI
- “Unsupported engine” warnings become fatal
Fix:
Pin Node in both local and CI.
Option A: .nvmrc
18
Option B: package.json engines
{
"engines": {
"node": ">=18 <=20"
}
}
Example GitHub Actions (Node pinned)
- uses: actions/setup-node@v4
with:
node-version: 18
cache: npm
- run: npm ci
- run: npm run build
If you’re stuck on Node 17 and see OpenSSL errors, use Node 16/18 instead. (Avoid the legacy OpenSSL workaround unless you have no choice.)
“Works on My Machine” = Case-Sensitive Import Failures
macOS/Windows often use case-insensitive file systems; many Linux production environments are case-sensitive.
Symptoms:
Module not found: Can't resolve './components/Button'- But the file is actually
./components/button.js
Fix:
Make the import case exactly match the file:
// wrong (file is button.jsx)
import Button from "./components/Button";// correct
import Button from "./components/button";
Tip: rename files to consistent casing conventions (e.g., PascalCase for components), and stick to it.
Missing Dependencies or Wrong Dependency Type
Production builds often fail because a dependency is:
- missing from
package.json - incorrectly placed in
devDependencies - installed locally but not committed in lockfile
Symptoms:
Module not found: Can't resolve 'xyz'Cannot find module 'xyz'
Fix:
Install and save correctly:
npm install xyz
# or if it truly is dev-only tooling
npm install -D xyz
If your production install excludes dev dependencies (common in some environments), anything required at build time must be in dependencies. CRA builds happen before deployment, so bundler plugins used during build must be available.
Environment Variables: CRA Only Exposes REACT_APP_*
CRA only injects env vars starting with REACT_APP_ (and a few built-ins). In production, missing env vars can crash builds if your code assumes they exist.
Symptoms:
Cannot read properties of undefined- runtime config referenced during build
Fix:
- Use the right prefix:
REACT_APP_API_BASE_URL=https://api.example.com
- Read it safely:
export const API_BASE_URL =
process.env.REACT_APP_API_BASE_URL ?? "https://fallback.example.com";
- Fail fast with a friendly error (recommended for production):
const required = ["REACT_APP_API_BASE_URL"];required.forEach((key) => {
if (!process.env[key]) {
throw new Error(`Missing required env var: ${key}`);
}
});
Note: CRA env vars are embedded at build-time. If you need runtime config (changing without rebuilding), you’ll need a separate strategy (e.g.,
config.jsonserved by your host).
“Treat Warnings as Errors” in CI (CI=true)
Some CI providers set CI=true automatically. CRA treats warnings as build failures when CI=true.
Symptoms:
- Build fails but the log shows only warnings like ESLint warnings
Fix options:
Best: fix the warnings.
Pragmatic: adjust ESLint rules or remove noisy warnings.
Last resort: override CI behavior (not recommended long-term).
# last resort
CI=false npm run build
Better: fix the common offenders:
- unused vars
- missing dependencies in hooks
- console warnings treated as lint warnings
Example fix for unused var:
//
const [count, setCount] = useState(0); // setCount unused triggers lint warning//
const [count, setCount] = useState(0);
// ...use setCount or remove it
Memory / Heap Issues (Build Dies Midway)
Large CRA apps can OOM in production build.
Symptoms:
JavaScript heap out of memory- build terminates without clear error
Fix:
Increase Node heap:
# macOS/Linux
export NODE_OPTIONS=--max_old_space_size=4096
npm run build
For CI, set an environment variable in your pipeline:
NODE_OPTIONS=--max_old_space_size=4096
Also reduce memory pressure:
- remove huge source maps if you don’t need them (or store them separately)
- split large dependencies (code splitting)
- avoid importing entire libraries when you only need one function
Broken Source Maps / Dependency Transpilation Issues
Some third-party packages ship modern JS that your build setup might not transpile correctly.
Symptoms:
- Babel parse errors in
node_modules Unexpected tokenin dependencies
Fix:
Try:
- upgrading the package
- swapping to a compatible build
- using an alternative library
Example: import the ESM/CJS entry the library recommends.
Also ensure your browserslist is reasonable:
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
Path / Public URL / Routing Issues
When deploying to a subpath (like /myapp/), builds can appear “broken” or assets 404.
Symptoms:
- app loads blank page
- console shows missing JS/CSS chunks
GET /static/js/main... 404
Fix: set homepage in package.json:
{
"homepage": "https://example.com/myapp"
}
Or for relative paths:
{
"homepage": "."
}
If using React Router (v6+) with a basename:
import { BrowserRouter } from "react-router-dom";export default function App() {
return (
<BrowserRouter basename="/myapp">
{/* routes */}
</BrowserRouter>
);
}
Common Code-Level Production Build Breakers
Using Node-only modules in the browser:
CRA doesn’t automatically polyfill Node core modules anymore.
Symptoms
Can't resolve 'fs'Can't resolve 'crypto'
Fix
Don’t use Node core modules in browser code. If you need hashing or crypto, use Web APIs or browser-friendly packages.
Example (Web Crypto API hashing):
export async function sha256(text) {
const data = new TextEncoder().encode(text);
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
return [...new Uint8Array(hashBuffer)]
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
Dynamic requires:
// breaks bundling often
const icon = require(`./icons/${name}.svg`);
Prefer static imports or a controlled map:
import HomeIcon from "./icons/home.svg";
import UserIcon from "./icons/user.svg";const icons = { home: HomeIcon, user: UserIcon };export function Icon({ name }) {
const Src = icons[name];
return Src ? <img alt="" src={Src} /> : null;
}
Bonus: A Small “Build Doctor” Script (Automate Checks)
Create scripts/build-doctor.js:
/* scripts/build-doctor.js */
const fs = require("fs");const pkg = JSON.parse(fs.readFileSync("./package.json", "utf8"));
const requiredEnv = ["REACT_APP_API_BASE_URL"];console.log("== Build Doctor ==");// Node version hint
console.log("Node:", process.version);// Engines check (soft)
if (pkg.engines?.node) {
console.log("package.json engines.node:", pkg.engines.node);
} else {
console.log("Tip: add engines.node to package.json to pin Node in CI.");
}// Env check
let missing = [];
for (const key of requiredEnv) {
if (!process.env[key]) missing.push(key);
}
if (missing.length) {
console.error("Missing env vars:", missing.join(", "));
process.exit(1);
}
console.log("Env vars OK.");// Homepage check
if (pkg.homepage) {
console.log("homepage:", pkg.homepage);
} else {
console.log("homepage not set (OK if deploying at root).");
}console.log("Build Doctor finished");
Run it before build:
{
"scripts": {
"prebuild": "node scripts/build-doctor.js",
"build": "react-scripts build"
}
}
When CRA Keeps Fighting You
CRA is stable but not very configurable. If you repeatedly hit build issues because you need deeper control (custom Webpack, advanced aliasing, dependency transpilation), consider migrating to Vite or Next.js. You’ll often get faster builds and clearer diagnostics.

