Site icon FSIBLOG

How to Fix Build Failures with create-react-app in Production

How to Fix Build Failures with create-react-app in Production

How to Fix Build Failures with create-react-app in Production

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:

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:

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:

Symptoms:

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:

Fix:

  1. Use the right prefix:
REACT_APP_API_BASE_URL=https://api.example.com
  1. Read it safely:
export const API_BASE_URL =
process.env.REACT_APP_API_BASE_URL ?? "https://fallback.example.com";
  1. 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.json served 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:

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:

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:

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:

Also reduce memory pressure:

Broken Source Maps / Dependency Transpilation Issues

Some third-party packages ship modern JS that your build setup might not transpile correctly.

Symptoms:

Fix:

Try:

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:

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

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.

Exit mobile version