Peakstack
Contents
Part II. Building10 min read

The doom loop: when the AI can't fix its own bug

You paste the error. It apologises. It rewrites the file. There is a new error. You paste that. It apologises again. Ninety minutes later the app is worse than when you started and you cannot remember what it used to do.

Every single person who has vibe coded has been here. It is not a sign that you are bad at this, and it is not a sign that the model is bad. It is a structural property of what happens when you ask a system to debug something it cannot observe.

Why the loop happens

The model cannot run your app. Sit with that, because everything follows from it. It cannot click the button, watch the network tab, or see what is actually in the database. It has your description of the symptom and a slice of the code, and from those two things it produces the most statistically plausible fix.

When the real cause is inside its blind spot, the most plausible fix is not the correct one. But the model has no way to know that, so it commits fully. It rewrites the file with total confidence. Now you have the original bug and an unnecessary rewrite. The next error is a little further from the truth than the last one, and each iteration pushes you further out.

The five ways out

1. Stop. Actually stop.

The strongest instinct in the loop is to send one more message, and it is exactly wrong. The next message is drawn from the same context that produced the last four failures, so it will fail in the same way. Close the chat. The loop is sustained by momentum and nothing else.

2. Go back to the last thing that worked

This is what the commits were for. git stash or git reset --hard to your last known-good commit and you have deleted the entire mess in one command, including the four speculative rewrites you have lost track of.

People resist this because it feels like throwing away work. It is throwing away broken work, and it is the single highest-value move available to you. Forty minutes of confused output is not an asset you are protecting. It is the problem.

Terminal
git stash                 # park the mess, keep it retrievable
git log --oneline -10     # find the last commit where it worked
git diff HEAD             # or: see exactly what changed since then

If you have not been committing, this chapter is where that decision finally sends the invoice.

3. Make the model explain before it edits

The single most effective anti-loop prompt, and it works because it forbids the behaviour that creates the loop:

The circuit breaker
Do not change any code yet.

Explain what is actually happening, step by step, from the moment I click
the button to the moment the error appears. Name the specific file and line
where your explanation says it goes wrong.

If you are not sure, say which of the possible causes you cannot rule out
and tell me what to check to distinguish between them.

That last paragraph is doing the heavy lifting. It gives the model a legitimate way to express uncertainty. Without it, the training pressure is to be helpful and decisive, so it guesses, and states the guess as fact. Give it an exit and it will often tell you, accurately, that it cannot see the thing it needs to see.

4. Get evidence it does not have

The loop is an information problem. So go get information. Print the thing. Log the actual value at the actual moment.

  • What is really in the variable? console.log it, right before the line that fails. Half of all doom loops end here. The value is undefined, and has been the whole time, three functions upstream of where you were both looking.
  • Is the request even happening? Open the browser Network tab and click the button. No request means the bug is in the frontend and every server-side fix was wasted. A request with a 401 means it is auth, not logic.
  • Is the row actually there? Open the database and look. Not the UI, the table. “It saved successfully” and “the row exists” are different claims, and vibe-coded apps are notorious for the first without the second.

Then paste what you found. A model given the real value, the real status code, and the real row is a different and dramatically better collaborator than one given “it does not work.”

5. Delete it and describe it again

If a feature has been rewritten five times and is still wrong, the feature is not the problem. The description is. Delete the code entirely, go back to the spec, and write it properly this time, in a fresh conversation, with the constraints stated up front.

This is almost always faster than continuing. A fresh model with a good spec takes ninety seconds. A poisoned thread arguing with itself takes the rest of the evening and does not converge.

The rule that prevents most loops

And watch specifically for fixes that work by removing a check. A model under pressure to make an error stop will delete the validation that raised it, loosen the type, wrap the whole thing in a try/catch that swallows the exception, or comment out the permission test. The error genuinely does go away. The app is now silently broken in a way that no longer tells you it is broken, which is the exact shape of half the findings in the failure atlas.

A 'fix' that made things worseVulnerable
// Before: this threw "user is not defined" and was, in fact, correct to.
if (!user) throw new Error("Not authenticated");
return await db.getPrivateNotes(user.id);

// After the model "fixed" it:
try {
  return await db.getPrivateNotes(user?.id);
} catch {
  return await db.getAllNotes();   // ships everyone's notes to a stranger
}

The error is gone. The auth check is also gone. The app looks fixed and now serves data to anyone who asks.

That is not a strawman. It is a compressed version of a real pattern we find in shipped applications, and it is the reason a clean error log is not evidence of a clean app.

When you’re ready to ship

Get an invite to PeakStack.

The bugs that survive a doom loop are the quiet ones: the check that got deleted to make an error go away, the catch block that now hides a failure. They don't show up in your console, and they don't show up in your demo. PeakStack drives your live app in a real browser, checks what your landing page promises against what the app actually does, and runs the security pass this chapter describes: exposed keys, open database rules, client-side admin gates. You get a letter grade, ranked fixes, and an honest list of what we couldn’t check.

Private beta. Join the waitlist for an invite.