Software Rescue

May 21, 2026

The Bugs AI Can't See: Why Senior Engineers Rescue Vibe-Coded Apps

Cursor swears it fixed the bug. Three times. It's still broken. You're starting to suspect that the AI doesn't actually understand what's wrong — it just keeps moving symptoms around. You're right. Here's what an experienced engineer sees that the AI doesn't.

AI writes code. It doesn't debug code.

Cursor, Claude, Copilot, Lovable, Replit Agent — these tools are genuinely great at generating code that looks right. They pattern-match against millions of examples and produce something plausible in seconds. For greenfield work, that's a superpower.

Debugging is a different skill. Debugging means reading code that already exists, building a mental model of how it behaves, finding the place where the model and reality disagree, and changing the code to close that gap. AI tools are weak at this in a specific way: they don't form a mental model. They scan, pattern-match against the symptom, and propose a fix that looks like the kind of thing that fixes that kind of bug. Sometimes that's enough. For the gnarly bugs, it isn't.

Worse, AI is confident either way. It writes a 12-line patch with a calm explanation of why it fixes the problem. It rarely says “I think this is a race condition but I'm not certain — can you confirm by running the code with these logs added?” That's the move a senior engineer makes. AI just commits to the fix.

The bug patterns that get AI tools stuck

After enough rescue engagements, you see the same families of bugs again and again. They share a structure: the symptom is in one place, the cause is in another, and the cause involves something the AI can't see from a single file.

1. Async race conditions across useEffect and state

The form submits twice when the user double-clicks. Or the page sometimes shows stale data from the previous user's session. Or the loading spinner stays up forever after a fast network response.

These are timing bugs. The code looks fine line by line. Reading any individual function, nothing is wrong. The bug lives in the interleaving of two async paths — a request that returned before its cleanup ran, a state update that lost the race, a stale promise resolving into a component that already moved on. AI tools tend to “fix” these by adding a guard clause that suppresses the visible symptom in the common case, leaving the race intact.

2. Closures capturing stale state

The button works the first time. The second time, it sends the old value. The AI adds a useCallback with the dependency array it thinks is right. The bug stays. The user files it again. The AI now adds a ref. Sometimes it works. Sometimes it doesn't.

The actual fix is to understand which closure is capturing which version of the state, and why React (or Solid, or Vue) re-creates the closure in one render but not another. That requires reading the render tree mentally and tracing which props change when. AI can describe the closure problem in the abstract. Applying that knowledge to your specific tangle of hooks is a different game.

3. Silent data corruption

Nobody is throwing errors. The app is up. But every now and then a customer reports that their record has the wrong value. The order was for 4 units; the invoice says 5. The booking shows the right time in the UI but the wrong time in the email.

These bugs come from invariants that the code never explicitly states. A nullable column with a default of empty string. A pagination loop that misses the last record when the page size divides cleanly. A timezone conversion that runs twice in some paths and once in others. AI doesn't infer the invariant — it sees code that handles the common case and signs off. A senior engineer reads the same code and asks “what does this assume that nobody wrote down?”

4. “Works on my machine” bugs

Timezones. File paths. Environment variables. Locale settings. Node versions. These bugs are invisible in the AI's context — the AI sees the code, not your laptop's system clock or the production container's missing env var. The fix isn't code; it's the realization that the runtime environment is part of the program. AI doesn't form that model on its own. A senior engineer does — and they instinctively log the inputs at the boundary so they can compare local and production directly.

5. Performance cliffs that hide in demo traffic

With ten users, the app feels snappy. At a thousand, the dashboard takes nine seconds to load. The AI looks at the dashboard component and tunes the render path. The actual bug is an N+1 query in the data fetch — one query for the list, then one query per row. It never fired in demo because the demo had three rows.

Other variants of this pattern: a missing index that makes a query do a full table scan, a synchronous loop blocking the event thread, a memo that doesn't actually memo because its dependency is a new object each render. The signal that you're looking at one of these is that performance falls off a cliff at a specific scale, not gradually. AI tools don't reason about scale unless you hand them the production query plan.

6. Auth bugs “fixed” by wrapping the symptom in try/catch

Users intermittently get logged out. AI adds a retry. Now they only get logged out half as often. AI adds a longer retry. Now the app sometimes spins forever instead of erroring. The bug — a token refresh race between two tabs writing to the same localStorage key — never gets touched.

This is the most common pattern we see in vibe-coded auth: the code wraps the unhappy path in defensive logic until the visible error rate drops, but the underlying flow is still broken. The bug re-emerges as a different shape — slow logins, mysterious 401s, session resurrection. A senior engineer maps the actual token lifecycle and fixes the race once.

What a senior engineer does differently

The job isn't typing — it's thinking. Here's the loop a senior engineer runs that AI tools, on their own, don't:

  • Reproduce reliably before touching code. A bug you can't reproduce is a bug you can't fix. Senior engineers spend the first hour writing the smallest possible reproduction. AI tools tend to start patching immediately.
  • State the invariant out loud. What does this code assume is always true? Once you write that down, the bug is usually obvious — it's the place where the assumption breaks. AI describes invariants when you ask, but won't volunteer them.
  • Distinguish symptom from cause. The thing the user reports is downstream of the actual bug. Always. Senior engineers trace upstream until they find the code that, if you changed it, would make every downstream symptom impossible. AI tends to stop at the symptom.
  • Write the failing test first. A test that fails because of the bug, and passes because of the fix, is the only way to know you actually fixed it. AI fixes rarely come with a regression test unless you explicitly ask for one — and even then, the test sometimes passes against the broken code.
  • Read the framework source when needed. React rerendering rules, ORM query builders, signal libraries, HTTP client retry policies. The bug is sometimes downstream of a library behavior the AI doesn't remember accurately. A senior engineer opens node_modules and reads.

When to bring in an experienced engineer

Not every bug needs a senior engineer. Most don't. But these patterns are worth calling for backup:

  • The bug has come back three times. Each “fix” was treating a symptom. Time to find the cause.
  • It only happens in production. The reproduction environment is part of the bug. Someone has to instrument the live system safely.
  • Customers report wrong data. Data corruption usually means an invariant violation. Don't patch this one with AI suggestions — find the broken assumption.
  • Performance fell off a cliff. Scale-induced bugs are upstream of the slow component. Profiling the right layer is the skill.
  • Adding a feature breaks something else. Architectural debt is showing. The fix is rebuilding part of the system, not patching at the call site.

Rebuilding vs rescuing

Most of the time the right move is to rescue what you have — see Vibe Coding Rescue for the playbook. Throwing away working code, even messy working code, costs you months and reintroduces bugs you already fixed.

But sometimes a module is past the point where patching is cheaper than rebuilding. When the same file is the source of half the bugs, when nobody (human or AI) can confidently change it, when the architecture has accumulated enough accidental coupling that adding a feature touches eight files — that's a rebuild signal. A senior engineer can tell the difference. AI usually can't, because AI doesn't track the cost of churn over time.

The rebuild itself is also a senior-engineering task. You're recreating behavior that nobody fully documented, preserving the things that work, fixing the things that don't, and migrating data without losing it. AI is a useful tool inside that process. It isn't the driver.

AI is the multiplier, not the substitute

The right frame for vibe-coded apps isn't “AI bad, humans good.” AI is excellent for generating scaffolding, exploring options, writing first drafts of code and tests. It's the cheapest way to get from zero to a working prototype that's ever existed.

The handoff happens at the bugs. When your prototype runs into a real user's real edge case, the work shifts from generating code to understanding code that already exists. That's where experienced engineering pays for itself — not by typing faster than AI, but by seeing the bugs AI can't.

Stuck on a bug your AI tools can’t crack?

30-minute call with an engineer, not a salesperson. We’ll talk through the problem, the fastest path forward, and whether we’re the right fit.

Book a Free Call