AI Code Review: Quality Control for Vibe Coding

# Quality Control and Code Review: Pitfalls and Anti-Patterns When you're vibing with AI coding assistants, the speed and convenience can be intoxicating. You describe what you want, and suddenly there's working code in front of you. But here's the uncomfortable truth: that code might look right, run correctly in basic scenarios, and still contain subtle bugs, security vulnerabilities, or architectural mistakes that will haunt you later. This lesson focuses on the specific pitfalls and anti-patterns that emerge when using AI-assisted development—the mistakes that even experienced developers make when they get too comfortable with their AI copilot. ## The Hidden Dangers of Generated Code AI-generated code has a deceptive quality: it looks professional. It follows conventions, uses proper naming, and often includes comments. This polish creates a false sense of security. Unlike the obviously hacky code you might write at 2 AM, AI-generated code doesn't *look* dangerous. ### The Over-Trust Anti-Pattern The most common pitfall is assuming that because code runs, it's correct. AI assistants are excellent pattern matchers, but they don't understand your business logic, your edge cases, or your security requirements. Consider this seemingly innocuous example: ```python # Prompt: "Create a function to validate email addresses" import re def validate_email(email): pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' return re.match(pattern, email) is not None # Usage if validate_email(user_input): send_email(user_input) ``` This code works. It validates email format. But it has problems: 1. **No length limits**: Someone could send a 10MB string 2. **No internationalization**: Doesn't handle unicode characters in domains 3. **Returns boolean instead of validated/sanitized output**: You're not using the match object 4. **No consideration for email spoofing**: Doesn't validate domain existence A better approach after AI generation: ```python import re from email_validator import validate_email as lib_validate_email, EmailNotValidError def validate_email(email, max_length=320): """ Validate email address with proper bounds and internationalization. Args: email: Email string to validate max_length: Maximum allowed length (RFC 5321 limit) Returns: Normalized email string if valid Raises: ValueError: If email is invalid """ if not email or len(email) > max_length: raise ValueError(f"Email must be between 1 and {max_length} characters") try: # Use battle-tested library for proper validation validated = lib_validate_email(email, check_deliverability=False) return validated.normalized except EmailNotValidError as e: raise ValueError(f"Invalid email: {str(e)}") ``` **The lesson**: Your AI gave you a starting point, not a finished product. You need to think about the edge cases, security implications, and whether there's a better approach (like using a well-tested library). ## Code Review Red Flags with AI-Generated Code When reviewing AI-generated code, certain patterns should immediately raise your suspicion. These anti-patterns appear frequently because AI models optimize for "looks right" rather than "is secure and maintainable." ### Anti-Pattern 1: String Concatenation in Queries AI models love string concatenation because it appears in their training data constantly. It's simple, direct, and works in examples. It's also a security nightmare. ```javascript // AI-generated code - DANGEROUS async function getUserByEmail(email) { const query = "SELECT * FROM users WHERE email = '" + email + "'"; return await db.query(query); } ``` This is a textbook SQL injection vulnerability. The fix is obvious to security-conscious developers, but AI might generate this because it matches patterns in older codebases. **Corrected version:** ```javascript async function getUserByEmail(email) { // Use parameterized queries ALWAYS const query = "SELECT id, name, email, created_at FROM users WHERE email = $1"; return await db.query(query, [email]); } ``` Note the additional improvement: selecting specific columns instead of `SELECT *`. AI often generates lazy queries that over-fetch data. ### Anti-Pattern 2: Error Swallowing AI assistants frequently generate code that catches errors but doesn't properly handle them: ```python # AI-generated code - PROBLEMATIC def process_payment(amount, card_token): try: result = payment_api.charge(amount, card_token) return result except Exception as e: print(f"Error: {e}") return None ``` This anti-pattern creates multiple problems: - Silent failures that are hard to debug - No logging for production monitoring - Returning `None` might cause errors downstream - Catching `Exception` is too broad **Better approach:** ```python import logging from typing import Optional from .exceptions import PaymentError, PaymentDeclinedError logger = logging.getLogger(__name__) def process_payment(amount: int, card_token: str) -> dict: """ Process payment charge. Args: amount: Amount in cents card_token: Tokenized card information Returns: Payment result dictionary Raises: PaymentError: For API failures PaymentDeclinedError: For declined transactions ValueError: For invalid inputs """ if amount <= 0: raise ValueError("Amount must be positive") try: result = payment_api.charge(amount, card_token) logger.info(f"Payment processed successfully: {result.get('id')}") return result except payment_api.DeclinedError as e: logger.warning(f"Payment declined: {e}") raise PaymentDeclinedError(str(e)) except payment_api.APIError as e: logger.error(f"Payment API error: {e}", exc_info=True) raise PaymentError(f"Payment processing failed: {e}") ``` ### Anti-Pattern 3: Race Conditions in Async Code AI models struggle with concurrency nuances. They'll happily generate code with race conditions: ```javascript // AI-generated code - HAS RACE CONDITION class UserCache { constructor() { this.cache = new Map(); } async getUser(userId) { if (this.cache.has(userId)) { return this.cache.get(userId); } const user = await fetchUserFromDB(userId); this.cache.set(userId, user); return user; } } ``` If `getUser()` is called twice simultaneously for the same `userId`, both calls will miss the cache and fetch from the database. This is a classic race condition. **Race-condition-safe version:** ```javascript class UserCache { constructor() { this.cache = new Map(); this.pending = new Map(); } async getUser(userId) { // Return cached value if (this.cache.has(userId)) { return this.cache.get(userId); } // Return pending promise if fetch in progress if (this.pending.has(userId)) { return this.pending.get(userId); } // Start new fetch const fetchPromise = this._fetchAndCache(userId); this.pending.set(userId, fetchPromise); try { return await fetchPromise; } finally { this.pending.delete(userId); } } async _fetchAndCache(userId) { const user = await fetchUserFromDB(userId); this.cache.set(userId, user); return user; } } ``` ## The Code Review Checklist for AI-Generated Code Don't review AI-generated code the same way you'd review human-written code. AI has different failure modes. Here's your checklist: ### Security Review 1. **Input validation**: Does every user input get validated and sanitized? 2. **Parameterized queries**: Are all database queries using parameter binding? 3. **Authentication checks**: Are authorization checks present and correct? 4. **Secrets management**: Are API keys, passwords, or tokens hardcoded? 5. **Output encoding**: Is user-generated content properly escaped before display? ```python # Common AI mistake: No input validation def update_user_profile(user_id, bio): db.execute(f"UPDATE users SET bio = ? WHERE id = ?", [bio, user_id]) # What you need to check: def update_user_profile(user_id: int, bio: str) -> None: # Validate inputs if not isinstance(user_id, int) or user_id <= 0: raise ValueError("Invalid user ID") # Check length limits if len(bio) > 500: raise ValueError("Bio must be 500 characters or less") # Sanitize HTML if bio is displayed as HTML from bleach import clean sanitized_bio = clean(bio, tags=[], strip=True) db.execute( "UPDATE users SET bio = ? WHERE id = ?", [sanitized_bio, user_id] ) ``` ### Performance Review AI often generates code that works but scales poorly: 1. **N+1 queries**: Does the code fetch related data in a loop? 2. **Missing indexes**: Are database queries filtering on unindexed columns? 3. **Unbounded operations**: Can any operation grow without limits? 4. **Unnecessary computations**: Is the same calculation repeated multiple times? ```python # AI-generated N+1 query problem def get_posts_with_authors(limit=10): posts = Post.objects.all()[:limit] result = [] for post in posts: # This triggers a new query for EACH post author = User.objects.get(id=post.author_id) result.append({ 'title': post.title, 'author_name': author.name }) return result # Optimized version using select_related def get_posts_with_authors(limit=10): posts = Post.objects.select_related('author').all()[:limit] return [{ 'title': post.title, 'author_name': post.author.name } for post in posts] ``` ### Architecture Review 1. **Separation of concerns**: Is business logic mixed with data access or presentation? 2. **Error boundaries**: Are errors handled at appropriate layers? 3. **Dependency injection**: Is the code testable, or does it create dependencies directly? 4. **Configuration**: Are environment-specific values extracted to config? ## Testing AI-Generated Code: Beyond the Happy Path AI assistants excel at the happy path. They'll generate code that works perfectly when everything goes right. Your job is to think about everything that can go wrong. ### Write Tests for Failure Scenarios ```typescript // AI might generate this function function parseUserAge(input: string): number { return parseInt(input); } // But you need to test these scenarios: describe('parseUserAge', () => { it('should parse valid age', () => { expect(parseUserAge('25')).toBe(25); }); it('should handle negative numbers', () => { expect(() => parseUserAge('-5')).toThrow(); }); it('should handle non-numeric input', () => { expect(() => parseUserAge('abc')).toThrow(); }); it('should handle empty string', () => { expect(() => parseUserAge('')).toThrow(); }); it('should handle very large numbers', () => { expect(() => parseUserAge('999999')).toThrow(); }); it('should handle decimal numbers', () => { expect(() => parseUserAge('25.5')).toThrow(); }); }); // Better implementation after thinking through edge cases function parseUserAge(input: string): number { const trimmed = input.trim(); if (trimmed === '') { throw new Error('Age cannot be empty'); } const age = parseInt(trimmed, 10); if (isNaN(age)) { throw new Error('Age must be a number'); } if (age < 0 || age > 150) { throw new Error('Age must be between 0 and 150'); } if (trimmed.includes('.')) { throw new Error('Age must be a whole number'); } return age; } ``` ## Recognizing When to Reject AI Suggestions Sometimes the best code review decision is rejecting AI-generated code entirely. Here are scenarios where you should start over: ### When the Architecture is Wrong If AI generates a complex solution when a simple one exists, or uses inappropriate patterns: ```python # AI generates this overly complex solution class UserManager: def __init__(self): self.factory = UserFactory() self.repository = UserRepository() self.validator = UserValidator() def create_user(self, data): validated_data = self.validator.validate(data) user = self.factory.create(validated_data) return self.repository.save(user) # When you really just need: def create_user(email: str, name: str) -> User: user = User(email=email, name=name) user.save() return user ``` ### When It Uses Deprecated or Risky Patterns AI training data includes old code. It might suggest outdated approaches: ```javascript // AI suggests old callback pattern function fetchData(callback) { http.get('/api/data', (response) => { let data = ''; response.on('data', (chunk) => data += chunk); response.on('end', () => callback(null, data)); response.on('error', (err) => callback(err)); }); } // Modern async/await is clearer and safer async function fetchData() { const response = await fetch('/api/data'); if (!response.ok) { throw new Error(`HTTP error ${response.status}`); } return await response.text(); } ``` ## Building Better Prompts to Avoid Anti-Patterns Prevention is better than cure. Craft your prompts to avoid common pitfalls: **Instead of:** > "Create a login function" **Try:** > "Create a login function that: > - Uses bcrypt for password comparison > - Implements rate limiting (max 5 attempts per 15 minutes) > - Returns a JWT token on success > - Logs failed attempts for security monitoring > - Uses parameterized queries to prevent SQL injection" **Instead of:** > "Add error handling to this function" **Try:** > "Add error handling that: > - Uses specific exception types, not broad Exception catches > - Logs errors with appropriate severity levels > - Doesn't expose sensitive information in error messages > - Re-raises errors that can't be handled locally" ## The Continuous Review Mindset Vibe coding doesn't mean abandoning code review—it means adapting it. Treat AI as a junior developer who writes clean-looking code but needs guidance on security, performance, and architecture. Key practices for ongoing quality: 1. **Never merge AI-generated code without human review** - even for "simple" changes 2. **Set up automated checks** - linters, security scanners, and tests catch what you might miss (see our [quality-control](/lessons/quality-control) lesson) 3. **Document AI-generated code** - mark sections that need extra scrutiny 4. **Learn from mistakes** - when AI creates a bug, understand why and improve your prompts 5. **Stay current** - AI models improve, but they also inherit new biases from recent training data ## Conclusion AI coding assistants are powerful tools that can dramatically increase your productivity. But that productivity gain evaporates if you spend weeks debugging security vulnerabilities or performance issues that slipped through. The developers who excel at vibe coding aren't those who blindly trust AI output—they're those who've developed a keen eye for the specific ways AI-generated code fails. They know where to look, what to question, and when to reject a suggestion entirely. Remember: AI writes code that looks right. Your job is to ensure it *is* right. Master that distinction, and you'll harness AI's power while avoiding its pitfalls. For more on maintaining code quality with AI assistance, check out our lessons on [hallucination-detection](/lessons/hallucination-detection) and [managing-tech-debt](/lessons/managing-tech-debt). And if you're struggling with knowing when to use AI, our [when-not-to-use-ai](/lessons/when-not-to-use-ai) and [over-reliance](/lessons/over-reliance) lessons provide crucial guidance.