Vibe Coding, Debugging
How to Catch Bugs Before Production

To catch bugs before production, you stack cheap, fast checks in front of every deploy so a defect has to get past several independent gates before it ever reaches a user: static analysis and types catch whole classes of errors at author time, tests catch broken behavior on every commit, and a runtime pass in a production-like environment catches the bugs that only appear when the code actually runs.
The mistake most teams make is relying on a single layer — usually "it compiled and looked fine locally." That catches the obvious bugs and misses exactly the ones that hurt in production: the
The mistake most teams make is relying on a single layer — usually "it compiled and looked fine locally." That catches the obvious bugs and misses exactly the ones that hurt in production: the
undefined that only appears with real data, the async race that only loses under latency, the flow that only breaks on a phone. This guide walks the problem → diagnosis → fix path for building that stack in a Next.js / JavaScript codebase.Join the Vibe Coding Community
Swap builds, trade feedback, and ship faster alongside other people vibe coding.
The One-Line Answer
You catch bugs before production by layering four independent checks — static analysis (ESLint), a type system (TypeScript), automated tests (unit + end-to-end), and a runtime pass against a running build — and wiring them so any one of them failing blocks the deploy. No single tool is enough; each layer catches what the others structurally cannot.
The word that matters is independent. Each layer sees a different slice of the failure space. Skip one and you leave a whole category of bugs with no gate in front of it.
The word that matters is independent. Each layer sees a different slice of the failure space. Skip one and you leave a whole category of bugs with no gate in front of it.
Why "It Works Locally" Isn't Bug Detection
Local success proves one thing: the happy path ran once, on your machine, with your data, at your network speed. Production is none of those. The bugs that survive to production are precisely the ones that don't reproduce on a fast laptop with a warm cache and a single well-behaved user — the system-spanning bugs that are brutal to chase down once they're already live.
Two categories of bug detection matter here, and most teams only do the first:
Static bug detection analyzes your source code without running it — ESLint, TypeScript, and similar code quality tools for JavaScript read the code and flag problems by inspection. Fast, runs on every keystroke, catches structural mistakes.
Runtime error detection watches the code while it actually executes — in tests, in a preview deploy, or under a simulated user. It's the only thing that catches bugs whose existence depends on real execution: a null from an API that's usually populated, a
Static analysis proves the code is well-formed. Runtime detection proves the code actually works. You need both, because a program can be perfectly typed and still be perfectly broken.
The rest of this guide builds the layers in the order you should adopt them — cheapest and earliest first.
Two categories of bug detection matter here, and most teams only do the first:
Static bug detection analyzes your source code without running it — ESLint, TypeScript, and similar code quality tools for JavaScript read the code and flag problems by inspection. Fast, runs on every keystroke, catches structural mistakes.
Runtime error detection watches the code while it actually executes — in tests, in a preview deploy, or under a simulated user. It's the only thing that catches bugs whose existence depends on real execution: a null from an API that's usually populated, a
useEffect that loops under a specific state, a handler that throws only on the third click.Static analysis proves the code is well-formed. Runtime detection proves the code actually works. You need both, because a program can be perfectly typed and still be perfectly broken.
The rest of this guide builds the layers in the order you should adopt them — cheapest and earliest first.
Layer 1 — Static Analysis (ESLint)
Static analysis is your first and cheapest gate. It reads the code and flags patterns that are bugs or bug-shaped: unused variables masking a typo, a missing
Turn on the Next.js ESLint config and treat warnings as failures in CI:
The
Static analysis is fast enough to run in your editor, so the feedback loop is seconds. But it can only find bugs that are visible in the shape of the code. It cannot tell you that your pricing logic is wrong. For that, you need the next layers.
await, a React Hook called conditionally, a useEffect with an incomplete dependency array (a classic source of stale-state bugs in Next.js).Turn on the Next.js ESLint config and treat warnings as failures in CI:
# next lint is deprecated in Next 16+; use the ESLint CLI directly
npx eslint . --max-warnings=0The
--max-warnings=0 flag is the important part. A warning nobody blocks on is a bug nobody fixes. For catching real defects rather than style nits, lean on eslint-plugin-react-hooks (dependency-array and rules-of-hooks violations) and the @typescript-eslint rules that flag floating promises and unsafe any usage — those map directly onto runtime crashes.Static analysis is fast enough to run in your editor, so the feedback loop is seconds. But it can only find bugs that are visible in the shape of the code. It cannot tell you that your pricing logic is wrong. For that, you need the next layers.
Layer 2 — Types (TypeScript in Strict Mode)
A type checker is static analysis with a much stronger claim: it proves that the values flowing through your program have the shapes you think they do. In a Next.js app the highest-value bugs it catches live at boundaries — API responses,
Turn on strict mode. Without it you're leaving most of the value on the table:
Then make the check a gate:
Types catch an enormous class of JavaScript bugs for free, on every commit, before a single test runs. But types describe shapes, not behavior. A function can be perfectly typed and still return the wrong number. That's Layer 3.
searchParams, form data, environment variables — where "this might be undefined" turns into a production crash.Turn on strict mode. Without it you're leaving most of the value on the table:
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true
}
}strict forces you to handle null and undefined instead of assuming them away — which is the single most common source of "cannot read properties of undefined" in production. noUncheckedIndexedAccess goes further and makes arr[i] return T | undefined, catching the off-by-one and empty-array bugs that array access hides.Then make the check a gate:
tsc --noEmit # type-check without building; fails CI on any type errorTypes catch an enormous class of JavaScript bugs for free, on every commit, before a single test runs. But types describe shapes, not behavior. A function can be perfectly typed and still return the wrong number. That's Layer 3.
Layer 3 — Automated Tests (Unit + End-to-End)
Tests are where you check that the code does the right thing, not just that it's well-formed. Two tiers, and you want both:
Unit tests (Vitest or Jest) cover logic in isolation — the pricing calculation, the date formatter, the reducer. They're fast, run on every commit, and pin down behavior so a future change can't silently break it. Aim them at the logic that would be expensive to get wrong, not at trivial getters.
End-to-end tests (Playwright is the standard for Next.js) drive a real browser against a running build and catch integration bugs that unit tests structurally can't see: the form that submits but never persists, the route that 500s, the button that's there but not clickable. This is your first taste of true runtime error detection — the app is actually executing. The same manual testing instincts you'd use by hand are exactly what you're automating here.
Run the full suite in CI on every pull request, and block merge on failure. Tests are the layer that turns "I think this still works" into "the machine confirmed it still works."
The catch: tests only cover the cases you thought to write. They're excellent at catching regressions in known flows and blind to the flow nobody imagined. Which is the gap the last layer exists to close.
Unit tests (Vitest or Jest) cover logic in isolation — the pricing calculation, the date formatter, the reducer. They're fast, run on every commit, and pin down behavior so a future change can't silently break it. Aim them at the logic that would be expensive to get wrong, not at trivial getters.
End-to-end tests (Playwright is the standard for Next.js) drive a real browser against a running build and catch integration bugs that unit tests structurally can't see: the form that submits but never persists, the route that 500s, the button that's there but not clickable. This is your first taste of true runtime error detection — the app is actually executing. The same manual testing instincts you'd use by hand are exactly what you're automating here.
// example.spec.ts — Playwright catches the bug a unit test can't
import { test, expect } from '@playwright/test'
test('signup actually creates an account', async ({ page }) => {
await page.goto('/signup')
await page.getByLabel('Email').fill('test@example.com')
await page.getByRole('button', { name: 'Create account' }).click()
await expect(page.getByText('Welcome')).toBeVisible()
})Run the full suite in CI on every pull request, and block merge on failure. Tests are the layer that turns "I think this still works" into "the machine confirmed it still works."
The catch: tests only cover the cases you thought to write. They're excellent at catching regressions in known flows and blind to the flow nobody imagined. Which is the gap the last layer exists to close.
Layer 4 — A Runtime Pass Against a Production-Like Build
The final gate is running the actual application, in an environment that resembles production, and watching what breaks. Three practical moves:
Always test a production build, never
Deploy every PR to a preview environment and smoke-test it. A preview deploy runs the real build with real environment wiring, so it surfaces the config and boundary bugs that never appear locally.
Instrument runtime errors with a React error boundary plus a reporting hook (
This layer catches the bugs that are, by definition, invisible to the previous three: the ones that only exist when the code runs, with real data, under real conditions. The limitation is that it usually needs a user to exercise the flow — which is why these bugs classically wait until production, where the "user" is a real customer hitting the bug for you.
Always test a production build, never
next dev. Dev mode is unoptimized, skips certain optimizations, and hides an entire class of bugs — hydration mismatches, 'use client'/server boundary errors, and build-time failures that only surface under next build && next start. It's the same reason a Next.js app can feel fast locally but slow in production: if you only ever run the dev server, you're not doing runtime detection of the code you'll actually ship.Deploy every PR to a preview environment and smoke-test it. A preview deploy runs the real build with real environment wiring, so it surfaces the config and boundary bugs that never appear locally.
Instrument runtime errors with a React error boundary plus a reporting hook (
app/error.tsx in the App Router, plus a monitoring service) so that any exception that does slip through is caught, logged, and attributed instead of silently white-screening a user.This layer catches the bugs that are, by definition, invisible to the previous three: the ones that only exist when the code runs, with real data, under real conditions. The limitation is that it usually needs a user to exercise the flow — which is why these bugs classically wait until production, where the "user" is a real customer hitting the bug for you.
Closing the Last Gap: Synthetic Users
The reason bugs reach production isn't that teams don't know how to test — it's that the highest-value runtime check, a real person using the app in a way you didn't script, is the one thing you can't run before you have users. If you have no traffic yet, you have no one to trip over the broken flow until launch day.
The way to close that gap is to manufacture the traffic: drive the running app with synthetic users — AI personas that behave like real people, at human speed, exploring flows nobody wrote a test for. That's runtime error detection without waiting for live users, and it reliably surfaces behavioral bugs and errors that no static tool or scripted test was looking for. (For the broader argument about treating simulated usage as a first-class runtime, see the Adaptive Software essay.)
The way to close that gap is to manufacture the traffic: drive the running app with synthetic users — AI personas that behave like real people, at human speed, exploring flows nobody wrote a test for. That's runtime error detection without waiting for live users, and it reliably surfaces behavioral bugs and errors that no static tool or scripted test was looking for. (For the broader argument about treating simulated usage as a first-class runtime, see the Adaptive Software essay.)
The Pre-Production Bug-Detection Checklist
• Static analysis: ESLint with react-hooks and @typescript-eslint, run with
• Types: TypeScript
• Unit tests: Vitest/Jest on the logic that's expensive to get wrong; block CI on failure.
• End-to-end tests: Playwright against a running build for critical flows (signup, checkout, core actions).
• Production build: test
• Preview deploys: every PR gets a preview environment and a smoke test.
• Runtime instrumentation:
• Synthetic runtime: exercise the app with simulated users to find the behavioral bugs no scripted test covers.
Each line is an independent gate. The goal is simple: make it so a bug has to defeat all of them to reach a user, instead of just defeating "it looked fine locally."
--max-warnings=0 in CI.• Types: TypeScript
strict: true and noUncheckedIndexedAccess: true, gated with tsc --noEmit.• Unit tests: Vitest/Jest on the logic that's expensive to get wrong; block CI on failure.
• End-to-end tests: Playwright against a running build for critical flows (signup, checkout, core actions).
• Production build: test
next build && next start, never next dev, before shipping.• Preview deploys: every PR gets a preview environment and a smoke test.
• Runtime instrumentation:
app/error.tsx error boundary plus a monitoring/reporting hook.• Synthetic runtime: exercise the app with simulated users to find the behavioral bugs no scripted test covers.
Each line is an independent gate. The goal is simple: make it so a bug has to defeat all of them to reach a user, instead of just defeating "it looked fine locally."
Find the Runtime Bugs Before Your Users Do
Building out every layer above is the right long-term move — but it's also weeks of setup, and the last layer, real runtime behavior, is the one you can't finish until you have traffic. That's the gap Differ closes on day one. Connect your GitHub repo and Differ runs a code scan on your very first import — no live traffic, no config — surfacing the security, performance, dependency, and code-quality issues a static pass can find. Then its simulator drives your running app with synthetic personas at human speed, catching the behavioral bugs and unknown runtime errors that only appear when a real-feeling user actually uses your app. It's the fastest way to catch bugs before production in your Next.js app, including the ones you'd otherwise only discover when a customer hit them first.
Connect your repo to Differ and get your free report →
Connect your repo to Differ and get your free report →