🛍️

Shopify Store Design

✍️

WordPress Websites

Custom Web Apps

📈

Meta Ads Marketing

Partner NetworkWorkPricingAboutContact
Get Started →
📞Call📋Get Quote
"Works on My Machine" Never Happened Because Lint Said No
Engineering Journal #009Intermediate

"Works on My Machine" Never Happened Because Lint Said No

7 min readBy Ajay Thakkar
Project:Digitrust Enterprise Systems
Stack:
Next.jsNode.jsESLintCI/CDTypeScript

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 useEffect arrays.
  • 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:

1// Weak Pipeline: Relies on default platform forgiveness
2{
3  "scripts": {
4    "dev": "next dev",
5    "build": "next build"
6  }
7}
8
9// Enterprise Pipeline: The Hard Gatekeeper
10{
11  "scripts": {
12    "dev": "next dev",
13    "lint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings=0",
14    "build": "npm run lint && next build"
15  }
16}
17

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:

1// A catastrophic configuration that disables safety nets
2module.exports = {
3  eslint: {
4    ignoreDuringBuilds: true,
5  },
6}
7

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:

1// The Linter prevents this catastrophic bug
2useEffect(() => {
3  const interval = setInterval(() => {
4    saveData(currentUserId);
5  }, 5000);
6  return () => clearInterval(interval);
7}, []); // 🚨 Linter Error: Missing dependency 'currentUserId'
8

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.

1{
2  "scripts": {
3    "lint": "eslint . --max-warnings=0",
4    "typecheck": "tsc --noEmit",
5    "build": "npm run lint && npm run typecheck && next build"
6  }
7}
8

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:

  1. Lint Phase: Is the logic structurally sound? (No unused variables, no missing dependencies).
  2. Type Phase: Are the data contracts perfectly aligned? (No passing strings into number fields).
  3. 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

Local development environments are inherently forgiving; they will mask memory leaks and stale closures that crash production. The bash '&&' operator is the most powerful quality assurance tool in a developer's arsenal, acting as a hard gatekeeper. Strict build pipelines eliminate the 'works on my machine' excuse by ensuring the codebase is structurally sound before compilation.

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

Was this article helpful?

Continue Reading

Why AI Disabled ESLint Instead of Fixing My CodeNext.js
·1 min read

Why AI Disabled ESLint Instead of Fixing My Code

An AI coding agent tried to silence dozens of lint errors by injecting eslint-disable comments instead of fixing the underlying bugs. Discover why treating the linter as the enemy is a dangerous optimization trap in automated software engineering.

Read article →
Why My Green Vercel Deployment Returned a 404Next.js
·1 min read

Why My Green Vercel Deployment Returned a 404

Vercel displayed a green deployment success badge, but my live production URL returned a pristine 404 Not Found. After five hours of debugging, I discovered the real culprit wasn't Next.js or Vercel—it was an AI-generated build command that silently deployed nothing.

Read article →
One Shopify CDN Image Broke My Detection EngineNext.js
·1 min read

One Shopify CDN Image Broke My Detection Engine

A single Shopify CDN image completely fooled my custom website detection engine into returning a false positive. Discover why this Next.js debugging journey taught me that true architectural server signals must always mathematically outweigh weak third-party media assets in automated auditing tools.

Read article →