Quality Control and Code Review
Implement rigorous code review processes to maintain quality standards for AI-generated code.
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:
# 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:
- No length limits: Someone could send a 10MB string
- No internationalization: Doesn't handle unicode characters in domains
- Returns boolean instead of validated/sanitized output: You're not using the match object
- No consideration for email spoofing: Doesn't validate domain existence
A better approach after AI generation:
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.
// 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:
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:
# 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
Nonemight cause errors downstream - Catching
Exceptionis too broad
Better approach:
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:
// 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:
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
- Input validation: Does every user input get validated and sanitized?
- Parameterized queries: Are all database queries using parameter binding?
- Authentication checks: Are authorization checks present and correct?
- Secrets management: Are API keys, passwords, or tokens hardcoded?
- Output encoding: Is user-generated content properly escaped before display?
# 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:
- N+1 queries: Does the code fetch related data in a loop?
- Missing indexes: Are database queries filtering on unindexed columns?
- Unbounded operations: Can any operation grow without limits?
- Unnecessary computations: Is the same calculation repeated multiple times?
# 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
- Separation of concerns: Is business logic mixed with data access or presentation?
- Error boundaries: Are errors handled at appropriate layers?
- Dependency injection: Is the code testable, or does it create dependencies directly?
- 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
// 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:
# 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:
// 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:
- Never merge AI-generated code without human review - even for "simple" changes
- Set up automated checks - linters, security scanners, and tests catch what you might miss (see our quality-control lesson)
- Document AI-generated code - mark sections that need extra scrutiny
- Learn from mistakes - when AI creates a bug, understand why and improve your prompts
- 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 and managing-tech-debt. And if you're struggling with knowing when to use AI, our when-not-to-use-ai and over-reliance lessons provide crucial guidance.