"Works on My Machine" Never Happened Because Lint Said No
The phrase 'works on my machine' is an engineering failure. Discover how adding a single command to your pipeline—npm run lint before build—eliminates deployment anxiety and forces developers to actually fix their code before it reaches production.
One of the most dangerous, frustrating, and ubiquitous phrases in software engineering is: "I don't understand, it works on my machine."
Every senior engineer has heard it. A junior developer writes a new feature, clicks around their local localhost:3000 environment, verifies that the button does indeed turn blue when clicked, and aggressively pushes the code to the main branch. Five minutes later, the Vercel production deployment crashes, or worse, the build succeeds but the live application throws a catastrophic hydration error for thousands of users.
The developer is confused because, in their local environment, everything was perfect.
The problem is that local development environments lie to you. They are designed to be fast, forgiving, and highly tolerant of sloppy code. If you want to build resilient enterprise systems, you cannot rely on human discipline or the forgiveness of a local development server. You must rely on cold, mathematical, automated gatekeepers.
The most powerful gatekeeper you can implement is modifying a single line in your package.json file.
---
1. The Illusion of Localhost
When you type npm run dev in a Next.js or React application, the bundler spins up a local development server. This server is optimized for Hot Module Replacement (HMR) and rapid iterative feedback.
Because it optimizes for developer speed, it will often silently ignore structural flaws. It will compile your code even if you have:
- Unused variables bloating the file.
- Missing dependencies in your
useEffectarrays. - Imported modules that are never actually utilized.
- Variables accessed before they are initialized (Temporal Dead Zone).
Your local machine also holds a massive amount of hidden context. You might have global dependencies installed that your production server lacks. You might have environment variables cached in your terminal session.
When code works locally but breaks in production, it is almost never a server-side anomaly. It means your code was fundamentally broken the entire time, but the development environment masked the symptoms.
---
2. The Anatomy of the Hard Gatekeeper
To stop bad code from reaching production, you must intercept it before the compilation phase even begins. This is done by chaining your build scripts in the package.json file.
Look at the difference between a weak build script and a fortified enterprise build script:
The magic lies entirely in the standard bash && (Logical AND) operator.
In a UNIX-based terminal, when you string two commands together with &&, the system executes the first command (npm run lint). If that command finishes and returns an exit code of 0 (meaning absolute success), only then will the terminal proceed to execute the second command (next build).
If the linter finds even a single unused variable, it throws a non-zero exit code (usually 1). The && operator acts as a hard circuit breaker. The pipeline terminates immediately. The next build command never fires. The deployment is aggressively blocked.
---
3. Why Next.js Defaults Are Not Always Enough
You might be wondering: "Doesn't Next.js automatically run ESLint during the build phase?"
Yes, by default, running next build will trigger Next.js's internal linting step. However, relying purely on the default framework behavior introduces two massive architectural risks in enterprise environments.
Risk 1: The "Ignore During Builds" Escape Hatch
When developers are rushed to push a hotfix, they often get frustrated by lint errors breaking their deployment. A common, catastrophic "solution" they find on StackOverflow is to quietly add this to their next.config.js:
If this flag is flipped, next build will completely ignore all structural errors and compile the broken code directly into production. By explicitly enforcing "build": "npm run lint && next build" in your package.json, you ensure that the linter runs independently as a strict Node script. Even if a developer attempts to bypass the Next.js internal config, the explicit script will catch them.
Risk 2: Failing Fast Saves CI/CD Money
Next.js allocates memory, parses configurations, and spins up Webpack before it initiates its internal linting phase. If you have a massive enterprise application, spinning up the build engine takes time and consumes expensive CI/CD compute minutes (like Vercel build minutes or GitHub Actions runners).
Running npm run lint explicitly before the build command ensures that if there is a silly syntax error, the pipeline fails in exactly 3 seconds. It "fails fast," saving you the time and financial cost of spinning up a heavy build engine just to fail three minutes later.
---
4. Catching the Invisible Killers
What exactly are we protecting the application from? Why is a strict linting gate so vital for enterprise architecture?
The linter is not just checking if your code looks pretty or if your indentation is correct. When properly configured (using plugins like eslint-plugin-react-hooks), it catches invisible logic bombs that humans routinely miss during code reviews.
The Stale Closure Trap:
Without strict linting, this code will compile perfectly. It will work locally on the developer's machine. But in production, because currentUserId was omitted from the dependency array, the useEffect hook will form a stale closure. It will repeatedly save data to the very first user who logged in, completely ignoring when the active user changes.
If your build pipeline had enforced npm run lint && next build, this data-corruption bug would have been blocked from ever reaching the main branch.
---
5. Expanding the Pipeline: The TypeScript Gate
Once your engineering team adapts to the strictness of the linting gate, the next logical step in enterprise architecture is to introduce explicit type-checking into the exact same pipeline.
The tsc --noEmit command tells the TypeScript compiler to read every single file in the project and verify the strict type architecture without actually generating any JavaScript files.
This creates a brutal, uncompromising, three-stage gauntlet:
- Lint Phase: Is the logic structurally sound? (No unused variables, no missing dependencies).
- Type Phase: Are the data contracts perfectly aligned? (No passing strings into number fields).
- Build Phase: Compile the verified, pristine code into an optimized production bundle.
If an application successfully survives that pipeline, the phrase "works on my machine" becomes obsolete. It works because the logic is mathematically and structurally sound, regardless of what machine it runs on.
---
6. The Psychological Shift in Engineering Culture
When you first implement a strict npm run lint && next build pipeline, your developers will likely complain.
They will claim it slows them down. They will complain that they have to spend five extra minutes fixing unused imports before they can push a commit. They will feel friction.
As an engineering leader, you must hold the line. Explain to them that fixing a missing dependency array locally takes exactly two minutes. Diagnosing a bizarre stale-data bug in production, writing a hotfix, running damage control with the client, and deploying a patch takes two entire days.
Eventually, the culture shifts. Developers stop fighting the linter and start treating it like a trusted co-pilot. Code reviews become significantly faster because human reviewers no longer have to waste time hunting for missing variables or bad syntax; the pipeline guarantees those do not exist.
You no longer argue about code quality in pull requests. You simply point to the pipeline and say, "Don't argue with me. Argue with the linter."
---
Conclusion
A successful, green production build is not just about translating JavaScript into browser-readable bundles. It is about proving structural integrity.
If your CI/CD pipeline allows code to deploy without first passing a strict, independent linting phase, you are playing Russian roulette with your infrastructure. You are choosing the temporary comfort of a fast deployment over the long-term survival of the business logic.
Adding npm run lint && to your build script is a five-second technical change that forces a permanent cultural shift. It removes deployment anxiety, drastically reduces production regressions, and definitively kills the "works on my machine" excuse forever.
---
---
💡 Key Engineering Takeaways
Frequently Asked Questions
Why should I run lint explicitly if Next.js already does it during the build?
Running `npm run lint` explicitly before `next build` allows you to customize strict flags (like `--max-warnings=0`), prevents developers from bypassing checks via `ignoreDuringBuilds` configurations, and ensures your CI/CD pipeline "fails fast" in seconds rather than wasting expensive build minutes spinning up Webpack.
What does the `&&` operator actually do in a package.json script?
The `&&` operator is a logical AND bash command. It tells the terminal to execute the first script, check its exit code, and *only* execute the second script if the first one returns a 0 (absolute success). If the linter fails, the script immediately aborts.
Why does code work on my local machine but fail in production?
Local development servers (like `next dev`) are highly forgiving. They prioritize speed and Hot Module Replacement (HMR) over strictness, often masking memory leaks, stale closures, unused variables, and missing environment variables that will immediately crash a hardened production server.
Can I bypass the lint command if I have a critical emergency hotfix?
While you theoretically can edit the `package.json` to bypass it, you never should. Emergency hotfixes are statistically the most likely deployments to contain severe, application-breaking syntax errors because the developer is panicked. The linter is most critical during an emergency.
What is the difference between ESLint and TypeScript in the build pipeline?
ESLint checks for structural logic flaws, syntax errors, and React-specific anti-patterns (like missing hook dependencies). TypeScript (`tsc --noEmit`) strictly verifies that your data shapes and variable types are mathematically correct across the entire application. Both are required for true enterprise stability.
Feedback



