Anti-Patterns & Warning Signs: When AI Development Goes Wrong
Not all AI-assisted development is productive. Sometimes the LLM leads you astray, wastes time, or introduces problems faster than it solves them. This document helps you recognize when things are going wrong and how to course-correct.
Core Warning Signs
1. You’re Explaining the Same Thing Repeatedly
Symptom: The LLM keeps forgetting or misunderstanding the same concept.
What’s happening: Context pollution or fundamental misunderstanding.
Fix:
- Start a fresh conversation with better initial context
- Explicitly document the concept in architecture or invariants docs
- Reference the documentation instead of re-explaining
Prevention: Include critical concepts in warm-up context for every session.
2. Code Quality Is Degrading
Symptom: Early code was good, but recent outputs are messy, inconsistent, or buggy.
What’s happening: Context drift, accumulated errors, or conversation fatigue.
Fix:
- Stop and review what you have
- Commit working code
- Start fresh session with checkpoint summary
- Refactor problematic code before continuing
Prevention: Regular checkpoints every 30-60 minutes.
3. Scope Is Expanding Uncontrollably
Symptom: Simple task balloons into extensive refactoring or feature additions.
What’s happening: Lack of clear boundaries, LLM over-engineering, or unclear requirements.
Fix:
- Stop immediately
- Review original goal
- Explicitly state: “We are ONLY doing [specific task]”
- Roll back scope creep
Prevention: Be extremely specific about what’s in-scope and out-of-scope.
4. You’re Accepting Code You Don’t Understand
Symptom: Merging AI outputs without fully grasping what they do.
What’s happening: Moving too fast, trusting the LLM too much.
Fix:
- STOP IMMEDIATELY
- Review code line-by-line
- Ask LLM to explain anything unclear
- Refactor for clarity if needed
- Never merge code you don’t understand
Prevention: Insist on clarity. Ask “Explain this line-by-line” if needed.
5. Tests Are Passing But Features Don’t Work
Symptom: Green tests, broken functionality.
What’s happening: Weak tests, tests testing the wrong thing, or tests written to match broken code.
Fix:
- Manually test the feature
- Review test assertions carefully
- Strengthen tests
- Delete meaningless tests
Prevention: Review tests as critically as production code.
6. You’re Constantly Debugging AI Output
Symptom: More time fixing AI code than it would take to write it yourself.
What’s happening: Wrong tool for the job, unclear prompts, or complex task poorly suited for AI.
Fix:
- Consider writing this code yourself
- Simplify the problem
- Break into smaller pieces
- Improve your prompts
Prevention: Know when AI helps and when it hinders.
7. Architecture Is Becoming Incoherent
Symptom: New code doesn’t fit existing patterns, inconsistent approaches.
What’s happening: LLM doesn’t understand your architecture or isn’t following it.
Fix:
- Stop adding features
- Review architecture doc
- Refactor recent changes to align
- Strengthen architecture-aware prompts
Prevention: Always reference architecture docs in prompts.
8. The LLM Keeps Suggesting the Same Failed Approach
Symptom: After you reject a solution, it suggests a trivial variation.
What’s happening: LLM locked into a pattern, not exploring alternatives.
Fix:
Stop suggesting variations of [failed approach].
Please try a completely different approach.
Consider: [alternative direction]
Prevention: Explicitly rule out dead-ends: “Do not use [X] approach.”
9. Security Is an Afterthought
Symptom: Code has obvious security flaws after implementation.
What’s happening: LLM prioritizing functionality over security.
Fix:
- Review all code for security issues
- Add security requirements to prompts
- Use security-focused bug hunt
Prevention: Explicitly require secure coding upfront: “Include input validation and prevent SQL injection.”
10. You’re In Analysis Paralysis
Symptom: LLM generates extensive plans but little working code.
What’s happening: Over-planning, under-doing.
Fix:
- Skip to implementation
- Build smallest working version
- Iterate from there
Prevention: “Show me a minimal working implementation first, we can improve it after.”
Anti-Pattern Catalog
Anti-Pattern: The Endless Refactor
What it looks like: Every change triggers a refactoring cascade.
Why it’s bad: Never actually ships features, increases risk.
Fix: Limit refactoring. “Make this change with minimal impact to existing code.”
Anti-Pattern: Framework Fever
What it looks like: LLM suggests complex frameworks or libraries for simple problems.
Why it’s bad: Unnecessary dependencies, over-engineering.
Example:
❌ "Use Redux for state management"
✅ "Use React useState for this simple case"
Fix: “Solve this with standard library / existing dependencies only.”
Anti-Pattern: Pattern Misapplication
What it looks like: Design patterns used where simple code would work.
Why it’s bad: Unnecessary complexity, harder to maintain.
Example:
❌ Singleton pattern for configuration
❌ Factory pattern for simple object creation
❌ Observer pattern for one-time events
Fix: “Use the simplest approach that works.”
Anti-Pattern: Abstraction Explosion
What it looks like: Layers of abstractions for minimal code.
Why it’s bad: Hard to understand, hard to debug.
Example:
❌ BaseManager → AbstractDataManager → UserDataManager → UserRepository
✅ UserRepository (just one class)
Fix: “Don’t create abstractions unless we need them in at least 3 places.”
Anti-Pattern: Testing Theater
What it looks like: Many tests, low actual coverage of important cases.
Why it’s bad: False sense of security, tests don’t catch real bugs.
Example:
❌ test('function exists', () => {
expect(myFunction).toBeDefined();
});
✅ test('handles null user correctly', () => {
expect(() => myFunction(null)).toThrow('User required');
});
Fix: “Write tests that verify behavior, not existence.”
Anti-Pattern: The God File
What it looks like: Single file growing to thousands of lines.
Why it’s bad: LLM can’t fully reason about it, hard to maintain.
Example: utils.js with 50 unrelated functions.
Fix: Split by responsibility. Use modularity review prompt.
Anti-Pattern: Comment-Driven Development
What it looks like: Heavy comments explaining confusing code.
Why it’s bad: Code should be self-explanatory.
Example:
❌ # Loop through users and check if they match
for u in users:
# Compare user id with target
if u.id == target_id:
return u
✅ def find_user_by_id(users, target_id):
return next((u for u in users if u.id == target_id), None)
Fix: “Refactor for clarity instead of adding comments.”
Anti-Pattern: Copy-Paste Coding
What it looks like: LLM generates similar code multiple times instead of refactoring.
Why it’s bad: Violates DRY, harder to maintain.
Fix: “Extract common logic into a shared function.”
Anti-Pattern: The Over-Engineered API
What it looks like: APIs with many options, parameters, overloads.
Why it’s bad: Confusing, hard to use correctly.
Example:
❌ def process(data, mode='sync', async_=False, callback=None,
error_handler=None, retry=True, timeout=None, ...)
Fix: “Create a simple API with sensible defaults.”
Anti-Pattern: Silent Failure Cascade
What it looks like: Errors caught and swallowed throughout the code.
Why it’s bad: Bugs hide, debugging is impossible.
Example:
❌ try:
critical_operation()
except:
pass # Silently fails
Fix: “Handle errors explicitly or let them propagate.”
Anti-Pattern: Debug-Level Exception Swallowing
What it looks like: Broad exception handlers that log at debug level instead of warning or error.
Why it’s bad: Catching Exception also catches AttributeError, TypeError, and KeyError — actual bugs in your code. Logging at debug means nobody sees them in production. Real bugs get silently ignored, and data falls through to incorrect fallback paths.
Example:
❌ try:
result = provider.normalize_trade(raw_data)
except Exception:
logger.debug("Provider detection failed") # Swallows real bugs
# Falls through to generic parser with wrong field mappings
✅ try:
result = provider.normalize_trade(raw_data)
except (ValueError, KeyError) as e:
logger.warning("Provider normalization failed: %s", e) # Visible, specific
Fix: Catch specific exceptions. If you must catch Exception, log at warning or error, not debug. If a broad catch silently redirects to a fallback path, you’re hiding data corruption.
Anti-Pattern: Sentinel Value Abuse
What it looks like: Using a legitimate business value (zero, empty string, -1) to mean “not yet set” or “missing.”
Why it’s bad: The sentinel value eventually occurs naturally, and the code silently treats real data as uninitialized. This is especially dangerous in financial code where zero means “breakeven trade” — not “PnL hasn’t been calculated.”
Example:
❌ if trade.pnl == 0.0:
# Meant to check "not yet calculated"
# But also skips real breakeven trades
trade.pnl = calculate_pnl(trade)
✅ if not trade.pnl_is_set:
# Explicit boolean flag — boring but correct
trade.pnl = calculate_pnl(trade)
trade.pnl_is_set = True
Fix: Use an explicit flag or Optional type. pnl: Optional[float] = None with if pnl is None is safe because None can’t naturally occur as a PnL value. Overloading real values as sentinels is a design trap that AI readily falls into because it produces shorter code.
Anti-Pattern: Implicit Cross-Component Contracts
What it looks like: Module A produces data that Module B consumes, but the contract between them is never written down — it’s just whatever Module A happens to output today.
Why it’s bad: LLMs generate each module in isolation. When you ask the LLM to add a field to Module A, it doesn’t automatically update Module B’s expectations. The contract drifts silently until something breaks at runtime.
Example:
# Module A adds a new boolean field
@dataclass
class Trade:
pnl: float = 0.0
pnl_is_set: bool = False # New field
# Module B (persistence) was generated in a different session
# and doesn't know about pnl_is_set
def save(trade):
db.insert(trade.market, trade.pnl) # pnl_is_set lost
def restore() -> Trade:
row = db.query(...)
return Trade(market=row.market, pnl=row.pnl) # pnl_is_set defaults to False
# All restored trades now have pnl_is_set=False
# FIFO calculator will overwrite legitimate PnL values
Fix: Document cross-component contracts in your invariants doc. When you add a field to a data model, grep for every site that creates, stores, restores, or serializes that model. Fix them all in the same change — model first, then persistence, then business logic, then tests.
Anti-Pattern: Partial Failure State Corruption
What it looks like: A multi-step operation modifies state as it goes, but if it fails partway through, the state is left half-modified.
Why it’s bad: The system is now in a state that no code path was designed to handle. Subsequent operations may produce incorrect results or crash.
Example:
def load_trades(session, file):
session.trades = [] # Cleared existing trades
for row in parse(file):
trade = normalize(row) # Crashes on row 50 of 100
session.trades.append(trade)
# If parse crashes: session.trades has 49 trades
# Previous 200 trades are gone, new file only half-loaded
Fix: Build the new state in a temporary variable. Only replace the old state after the entire operation succeeds:
def load_trades(session, file):
new_trades = []
for row in parse(file):
new_trades.append(normalize(row))
session.trades = new_trades # Atomic swap on success
Anti-Pattern: Fix One, Miss Ten
What it looks like: You find a bug (e.g., hardcoded $ symbol, or vars(t) instead of t.to_dict()), fix it in the file where you found it, and move on.
Why it’s bad: LLM-generated code often replicates the same mistake across many files — the same pattern was generated during different sessions. Fixing one instance and missing nine others means the bug persists.
Example:
Audit finding: "vars(t) bypasses to_dict() sanitization"
Fix applied: report_data.py ✅
Still broken:
- gui.py ❌
- api/services/trade_service.py ❌
- persistence.py ❌
- serializers.py ❌
Fix: After fixing any bug, search the entire codebase for the same pattern. Fix the class of bugs, not the instance. Run: grep -r 'vars(t)' . or grep -r 'hardcoded_value' . Every fix should include a codebase-wide grep.
Anti-Pattern: Database Query Explosion
What it looks like: N+1 queries, missing eager loading.
Why it’s bad: Terrible performance.
Example:
❌ for user in users:
user.posts # Separate query for each user!
Fix: Run performance review prompt after database code.
Anti-Pattern: The Symptom Chase
What it looks like: Adding defensive checks in 5+ locations for the same recurring error.
Why it’s bad: You’re masking the root cause instead of fixing it. The bug persists, just appearing in new locations.
Real example (20 commits to change one number):
Commit 1: Add sanitization to toContact mapper
Commit 2: Add type checks to BD page
Commit 3: Add sanitization to all role-based pages
Commit 4: Add contact_id handling and debug logging
Commit 5: Fix unsafe contact_id usages
Commit 6: Add render-time debug logging
Commit 7: Add render-time sanitization for stageContacts
Commit 8: Add sanitization to CommandPalette, Dashboard
Commit 9: Add KanbanErrorBoundary to isolate crash
Commit 10: Enhanced debug logging
Commit 11: Remove Avatar component to isolate crash
Commit 12: Remove Avatar from more components
Commit 13: Radical simplification - plain HTML only
Commit 14: Bypass ALL wrappers
Commit 15: Add back SmartColumn... crash returns! ← Root cause found
Commit 16: Disable virtualization ← Actual fix: 1 line
The pattern:
- Error: “Objects are not valid as React child”
- Commits 1-14: Adding defensive type checks everywhere the error appeared
- Commit 15: Started removing code instead of adding → found virtualization was the cause
- Commit 16: Set
VIRTUALIZATION_THRESHOLD = 10000(one constant)
flowchart LR
subgraph symptom ["❌ Symptom Chasing (14 commits)"]
direction TB
S1[Add check to mapper] --> S2[Add check to BD page]
S2 --> S3[Add check to role pages]
S3 --> S4[Add check to Dashboard]
S4 --> S5[Add check to CommandPalette]
S5 --> S6["...more files"]
end
subgraph root ["✅ Root Cause Fix (1 line)"]
direction TB
R1["Disable virtualization<br/>THRESHOLD = 10000"]
end
symptom -.->|"Error keeps<br/>appearing elsewhere"| symptom
root -.->|"Error gone<br/>everywhere"| Done["✓ Fixed"]
style symptom fill:#fff0f0,stroke:#c00
style root fill:#f0fff0,stroke:#0a0
style Done fill:#efe,stroke:#0a0
Red flags you’re symptom chasing:
- Same error type appearing in new locations after each “fix”
- Adding identical defensive checks across multiple files
- Grepping for patterns (
contact.name) instead of tracing data flow - Commit messages like “Add more sanitization” or “Fix in another location”
Fix: Use root-cause-isolation prompt:
- Read the stack trace (it said
setData→ cache mutation, not rendering) - Trace data upstream (API → cache → component)
- Disable complexity layers one by one (virtualization, memoization, caching)
- Find the single boundary where invalid data enters
- Fix there, once
Prevention: When you’re about to add the same defensive check in a third location, stop. You’re treating symptoms. Trace upstream instead.
Recognizing Context Pollution
Signs context is polluted:
- LLM contradicts itself
- Suggests code violating earlier constraints
- “Forgets” decisions from same conversation
- Responses become generic/vague
- Solutions ignore project-specific context
Recovery:
- Generate checkpoint summary
- Commit working code
- Start fresh conversation
- Provide corrected context upfront
When AI Is The Wrong Tool
AI struggles with:
- Highly complex algorithms requiring deep reasoning
- Code requiring deep domain expertise
- Performance-critical low-level code
- Novel approaches (AI prefers common patterns)
- Debugging race conditions or subtle timing issues
When to code yourself:
- You could write it faster than explaining it
- Security is critical and complexity is high
- Performance requirements are stringent
- You’re learning a new technology (don’t let AI rob you of understanding)
AI excels at:
- Boilerplate generation
- Standard CRUD operations
- Test case generation
- Refactoring for clarity
- Documentation
- Common patterns implementation
Course Correction Strategies
Hard Reset
When things are seriously off track:
- Stop coding
- Commit or stash work
- Review roadmap and architecture
- Identify what went wrong
- Start fresh with corrected approach
- Cherry-pick good code from failed attempt
Gentle Correction
For minor drift:
- Checkpoint current state
- Explicitly state the issue
- Reference docs (architecture, invariants)
- Request specific correction
- Verify fix before continuing
Scope Reset
When scope has creeped:
Let's reset scope. We are ONLY doing:
- [Specific task 1]
- [Specific task 2]
We are NOT doing:
- [Out of scope item 1]
- [Out of scope item 2]
Please focus exclusively on the in-scope items.
Velocity Red Flags
You should be moving faster with AI, not slower.
If you’re experiencing:
- More debugging than writing
- More explaining than coding
- More refactoring than progressing
- More testing than implementing
Something is wrong. Time to reassess.
Quality Red Flags
Code quality should remain high.
If you’re seeing:
- Declining test coverage
- More bugs in new code
- Harder-to-read code
- Architecture drift
Stop and course-correct.
Trust Red Flags
You should trust the code you’re merging.
If you’re:
- Merging code you don’t understand
- Skipping review “just this once”
- Accepting solutions that feel wrong
- Ignoring security concerns
You’ve lost control. Reset immediately.
Emergency Checklist
When things feel wrong:
- Stop adding features
- Run all tests
- Review recent commits
- Check architecture alignment
- Verify invariants hold
- Review security
- Generate checkpoint summary
- Identify specific problems
- Start fresh or roll back
Don’t dig deeper when you’re in a hole.
Prevention Is Better Than Cure
Best practices to avoid anti-patterns:
- ✅ Clear, specific prompts
- ✅ Reference architecture and invariants
- ✅ Regular checkpoints
- ✅ Strict code review
- ✅ Human review for critical code
- ✅ Test everything
- ✅ Start fresh when confused
- ✅ Push back on bad suggestions
- ✅ Maintain focus
- ✅ Know when to code yourself
Remember
AI amplifies both good and bad practices.
- Good process + AI = Velocity
- Bad process + AI = Faster accumulation of technical debt
Your job is to maintain discipline.
The LLM will happily lead you off a cliff if you let it. Stay vigilant, trust your judgment, and course-correct early.
When in doubt, slow down and review.