Let me start with a hard truth: That code your AI assistant just wrote for you? It's probably riddled with issues.
I learned this through years of working with AI-generated code, reviewing countless implementations, and seeing the same mistakes repeated across different projects and teams. The pattern is clear: developers who blindly trust AI output often face preventable problems, while those who treat it as a starting point that requires careful review build robust, maintainable systems.
After analyzing thousands of lines of AI-generated code across various projects, I've identified patterns that separate good AI-assisted development from problematic code. This article shares those insights to help you write better code with AI assistance.
Table of Contents
- A Common Scenario: When AI Code Fails
- The 7 Deadly Sins of AI-Generated Code
- Security Vulnerabilities You're Probably Missing
- How to Properly Test AI Output
- Real Horror Stories (And Lessons Learned)
- The AI Code Review Checklist
- Building Better Prompts for Better Code
- Conclusion: AI is a Tool, Not a Replacement
A Common Scenario: When AI Code Fails
Picture this: You need to create an endpoint for bulk user data export. You ask your AI assistant for help. The code looks clean, works in local tests, and passes basic checks. You integrate it into your application.
Then issues start appearing. The database struggles under load. Users report slow responses. Investigation reveals the AI generated code without pagination, proper indexing considerations, or rate limiting. Here's a typical example:
// ❌ The AI-generated code that caused the outage
app.get('/api/users/export', async (req, res) => {
const users = await User.find({}).lean();
res.json(users);
});
Looks innocent, right? Wrong. Here's what was missing:
// ✅ What it should have been
app.get('/api/users/export', async (req, res) => {
try {
// Authentication & Authorization
if (!req.user || !req.user.hasRole('admin')) {
return res.status(403).json({ error: 'Unauthorized' });
}
// Rate limiting (should be middleware, but shown here for clarity)
const limit = await rateLimiter.check(req.user.id, 'export', 1, 3600);
if (!limit.allowed) {
return res.status(429).json({
error: 'Rate limit exceeded',
retryAfter: limit.retryAfter
});
}
// Pagination & limits
const page = parseInt(req.query.page) || 1;
const limit = Math.min(parseInt(req.query.limit) || 100, 1000);
const skip = (page - 1) * limit;
// Efficient query with field selection
const users = await User
.find({})
.select('-password -refreshToken -__v')
.skip(skip)
.limit(limit)
.lean()
.hint({ email: 1 }); // Use existing index
const total = await User.countDocuments();
// Proper response structure
res.json({
data: users,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit)
}
});
} catch (error) {
console.error('Export error:', error);
res.status(500).json({
error: 'Export failed',
message: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
});
The problem: Without these critical features, the code can cause performance issues, database strain, and poor user experience under real-world conditions.
The 7 Deadly Sins of AI-Generated Code
After analyzing hundreds of AI code failures, I've identified seven recurring issues:
1. Missing Error Handling
AI loves happy path code. It rarely considers what happens when things go wrong.
# ❌ AI-generated code
def process_payment(amount, card_token):
charge = payment_gateway.create_charge(
amount=amount,
currency='usd',
source=card_token
)
return charge.id
# ✅ Production-ready version
def process_payment(amount: int, card_token: str) -> Dict[str, Any]:
"""
Process payment with comprehensive error handling.
Args:
amount: Amount in cents
card_token: Payment gateway card token
Returns:
Dict with charge_id and status
Raises:
PaymentError: If payment processing fails
"""
if amount <= 0:
raise ValueError("Amount must be positive")
if not card_token or not card_token.startswith('tok_'):
raise ValueError("Invalid card token")
try:
charge = payment_gateway.create_charge(
amount=amount,
currency='usd',
source=card_token,
idempotency_key=f"charge_{uuid.uuid4()}" # Prevent duplicate charges
)
logger.info(f"Payment processed: {charge.id}", extra={
'amount': amount,
'charge_id': charge.id
})
return {
'charge_id': charge.id,
'status': charge.status,
'amount': charge.amount
}
except payment_gateway.CardError as e:
# Card was declined
logger.warning(f"Card declined: {e.user_message}")
raise PaymentError(f"Card declined: {e.user_message}")
except payment_gateway.RateLimitError as e:
# Too many requests
logger.error("Payment gateway rate limit hit")
raise PaymentError("Payment service temporarily unavailable")
except payment_gateway.InvalidRequestError as e:
# Invalid parameters
logger.error(f"Invalid payment request: {str(e)}")
raise PaymentError("Invalid payment parameters")
except payment_gateway.APIConnectionError as e:
# Network problem
logger.error(f"Payment gateway connection error: {str(e)}")
raise PaymentError("Payment service connection failed")
except Exception as e:
# Catch-all for unexpected errors
logger.exception("Unexpected payment error")
raise PaymentError("Payment processing failed")
2. Security Vulnerabilities
AI doesn't think like a hacker. It generates code that works, not code that's secure.
// ❌ SQL Injection waiting to happen
app.get('/users/search', (req, res) => {
const query = `SELECT * FROM users WHERE name LIKE '%${req.query.name}%'`;
db.query(query, (err, results) => {
res.json(results);
});
});
// ✅ Parameterized queries
app.get('/users/search', async (req, res) => {
try {
// Input validation
const schema = Joi.object({
name: Joi.string().max(100).required(),
page: Joi.number().integer().min(1).default(1),
limit: Joi.number().integer().min(1).max(100).default(20)
});
const { name, page, limit } = await schema.validateAsync(req.query);
const offset = (page - 1) * limit;
// Parameterized query prevents SQL injection
const results = await db.query(
'SELECT id, name, email FROM users WHERE name ILIKE $1 LIMIT $2 OFFSET $3',
[`%${name}%`, limit, offset]
);
res.json({
data: results.rows,
pagination: { page, limit }
});
} catch (error) {
if (error.isJoi) {
return res.status(400).json({ error: error.details[0].message });
}
logger.error('Search error:', error);
res.status(500).json({ error: 'Search failed' });
}
});
3. No Input Validation
AI assumes all inputs are valid and well-formatted.
// ❌ No validation
function createUser(userData: any) {
return db.users.create(userData);
}
// ✅ Proper validation with Zod
import { z } from 'zod';
const userSchema = z.object({
email: z.string().email().max(255),
password: z.string().min(8).max(128)
.regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])/,
'Password must contain uppercase, lowercase, number, and special character'),
name: z.string().min(2).max(100).trim(),
age: z.number().int().min(18).max(120).optional(),
role: z.enum(['user', 'admin', 'moderator']).default('user')
});
type UserInput = z.infer<typeof userSchema>;
async function createUser(userData: unknown): Promise<User> {
try {
// Validate and parse
const validatedData = userSchema.parse(userData);
// Check if email exists
const existing = await db.users.findOne({ email: validatedData.email });
if (existing) {
throw new Error('Email already registered');
}
// Hash password
const hashedPassword = await bcrypt.hash(validatedData.password, 12);
// Create user
const user = await db.users.create({
...validatedData,
password: hashedPassword,
createdAt: new Date(),
emailVerified: false
});
// Don't return password
const { password, ...userWithoutPassword } = user;
return userWithoutPassword;
} catch (error) {
if (error instanceof z.ZodError) {
throw new ValidationError(error.errors);
}
throw error;
}
}
4. Resource Leaks
AI often forgets to clean up connections, file handles, and other resources.
# ❌ Resource leak
def process_large_file(filename):
file = open(filename, 'r')
data = file.read()
return process_data(data)
# File never closed! Memory leak!
# ✅ Proper resource management
from contextlib import contextmanager
import psycopg2
@contextmanager
def get_db_connection():
"""Context manager for database connections."""
conn = None
try:
conn = psycopg2.connect(
host=os.getenv('DB_HOST'),
database=os.getenv('DB_NAME'),
user=os.getenv('DB_USER'),
password=os.getenv('DB_PASSWORD'),
connect_timeout=5
)
yield conn
conn.commit()
except Exception as e:
if conn:
conn.rollback()
raise
finally:
if conn:
conn.close()
def process_large_file(filename: str) -> dict:
"""Process file with proper resource management."""
try:
with open(filename, 'r', encoding='utf-8') as file:
# File automatically closed after this block
with get_db_connection() as conn:
# Connection automatically managed
cursor = conn.cursor()
# Process in chunks to avoid memory issues
chunk_size = 1000
for chunk in iter(lambda: file.read(chunk_size), ''):
data = process_data(chunk)
cursor.execute(
"INSERT INTO processed_data (content) VALUES (%s)",
(data,)
)
cursor.close()
return {'status': 'success', 'file': filename}
except FileNotFoundError:
raise ValueError(f"File not found: {filename}")
except MemoryError:
raise ValueError("File too large to process")
5. Performance Killers
The infamous N+1 query problem and other performance issues are AI's specialty.
// ❌ N+1 Query Problem
async function getUsersWithPosts() {
const users = await User.findAll();
for (const user of users) {
user.posts = await Post.findAll({ where: { userId: user.id } });
}
return users;
}
// This makes 1 query for users + N queries for posts = N+1 queries!
// ✅ Optimized with eager loading
async function getUsersWithPosts() {
const users = await User.findAll({
include: [{
model: Post,
as: 'posts',
required: false,
limit: 10, // Limit posts per user
order: [['createdAt', 'DESC']]
}],
limit: 100, // Limit total users
subQuery: false // Prevent Sequelize from generating subqueries
});
return users;
}
// Just 1 optimized query with JOIN!
// ✅ Or use DataLoader for GraphQL
const postLoader = new DataLoader(async (userIds) => {
const posts = await Post.findAll({
where: { userId: { [Op.in]: userIds } }
});
// Group posts by userId
const postsByUserId = {};
posts.forEach(post => {
if (!postsByUserId[post.userId]) {
postsByUserId[post.userId] = [];
}
postsByUserId[post.userId].push(post);
});
// Return in same order as input
return userIds.map(id => postsByUserId[id] || []);
});
6. Hardcoded Values & Magic Numbers
AI loves to hardcode values instead of using configuration.
// ❌ Hardcoded nightmare
func ProcessOrders() {
timeout := time.Second * 30 // Magic number
maxRetries := 3 // Magic number
batchSize := 100 // Magic number
// ... processing logic
}
// ✅ Configuration-driven
type OrderProcessorConfig struct {
Timeout time.Duration
MaxRetries int
BatchSize int
EnableMetrics bool
WorkerCount int
}
func NewOrderProcessorConfig() *OrderProcessorConfig {
return &OrderProcessorConfig{
Timeout: getEnvDuration("ORDER_TIMEOUT", 30*time.Second),
MaxRetries: getEnvInt("ORDER_MAX_RETRIES", 3),
BatchSize: getEnvInt("ORDER_BATCH_SIZE", 100),
EnableMetrics: getEnvBool("ENABLE_METRICS", true),
WorkerCount: getEnvInt("WORKER_COUNT", runtime.NumCPU()),
}
}
type OrderProcessor struct {
config *OrderProcessorConfig
logger *log.Logger
metrics *Metrics
}
func (p *OrderProcessor) ProcessOrders(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, p.config.Timeout)
defer cancel()
// Use configured values
for attempt := 0; attempt < p.config.MaxRetries; attempt++ {
if err := p.processBatch(ctx, p.config.BatchSize); err != nil {
p.logger.Printf("Attempt %d failed: %v", attempt+1, err)
continue
}
return nil
}
return fmt.Errorf("failed after %d retries", p.config.MaxRetries)
}
7. Missing Logging & Observability
When things go wrong in production, you'll wish AI had added proper logging.
// ❌ Silent failures
public void processPayment(Payment payment) {
try {
paymentGateway.charge(payment);
database.save(payment);
} catch (Exception e) {
// Silently fails - good luck debugging!
}
}
// ✅ Comprehensive logging and observability
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
public class PaymentProcessor {
private static final Logger log = LoggerFactory.getLogger(PaymentProcessor.class);
private final PaymentGateway gateway;
private final Database database;
private final MeterRegistry metrics;
private final Timer paymentTimer;
public PaymentProcessor(PaymentGateway gateway, Database database, MeterRegistry metrics) {
this.gateway = gateway;
this.database = database;
this.metrics = metrics;
this.paymentTimer = metrics.timer("payment.processing.time");
}
public PaymentResult processPayment(Payment payment) {
Timer.Sample sample = Timer.start(metrics);
String paymentId = payment.getId();
log.info("Processing payment: id={}, amount={}, currency={}",
paymentId, payment.getAmount(), payment.getCurrency());
try {
// Add correlation ID for tracing
MDC.put("paymentId", paymentId);
MDC.put("userId", payment.getUserId());
// Charge the payment
ChargeResult chargeResult = gateway.charge(payment);
log.info("Payment charged successfully: id={}, gatewayRef={}",
paymentId, chargeResult.getReference());
// Save to database
payment.setGatewayReference(chargeResult.getReference());
payment.setStatus(PaymentStatus.COMPLETED);
database.save(payment);
log.info("Payment saved to database: id={}", paymentId);
// Record metrics
metrics.counter("payment.success",
"currency", payment.getCurrency()).increment();
sample.stop(paymentTimer);
return PaymentResult.success(chargeResult);
} catch (GatewayException e) {
log.error("Payment gateway error: id={}, error={}",
paymentId, e.getMessage(), e);
metrics.counter("payment.gateway.error",
"errorType", e.getClass().getSimpleName()).increment();
payment.setStatus(PaymentStatus.FAILED);
payment.setErrorMessage(e.getMessage());
database.save(payment);
return PaymentResult.failure("Gateway error: " + e.getMessage());
} catch (DatabaseException e) {
log.error("Database error while saving payment: id={}",
paymentId, e);
metrics.counter("payment.database.error").increment();
// Payment might be charged but not saved - needs investigation
log.warn("CRITICAL: Payment may be charged but not recorded: id={}",
paymentId);
return PaymentResult.failure("Database error - please contact support");
} catch (Exception e) {
log.error("Unexpected error processing payment: id={}",
paymentId, e);
metrics.counter("payment.unexpected.error").increment();
return PaymentResult.failure("Unexpected error occurred");
} finally {
MDC.clear();
sample.stop(paymentTimer);
}
}
}
Security Vulnerabilities You're Probably Missing
AI-generated code often contains subtle security issues that pass basic code review. Here are the most dangerous ones:
1. Authentication Bypass
# ❌ AI might generate this
@app.route('/api/admin/users')
def get_users():
if request.headers.get('X-Admin-Token') == 'admin123':
return jsonify(User.query.all())
return jsonify({'error': 'Unauthorized'}), 401
# Problems:
# - Hardcoded token
# - No rate limiting
# - Timing attack vulnerable
# - Returns all user data including passwords
# ✅ Proper implementation
from functools import wraps
import hmac
import hashlib
from flask_limiter import Limiter
limiter = Limiter(app, key_func=lambda: request.remote_addr)
def require_admin(f):
@wraps(f)
@limiter.limit("10 per minute")
def decorated_function(*args, **kwargs):
token = request.headers.get('Authorization', '').replace('Bearer ', '')
if not token:
return jsonify({'error': 'No token provided'}), 401
try:
# Verify JWT token
payload = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
# Check expiration
if payload.get('exp', 0) < time.time():
return jsonify({'error': 'Token expired'}), 401
# Check role (constant-time comparison)
if not hmac.compare_digest(payload.get('role', ''), 'admin'):
return jsonify({'error': 'Insufficient permissions'}), 403
# Add user to request context
request.current_user = payload
return f(*args, **kwargs)
except jwt.InvalidTokenError as e:
return jsonify({'error': 'Invalid token'}), 401
return decorated_function
@app.route('/api/admin/users')
@require_admin
def get_users():
# Only return safe fields
users = User.query.with_entities(
User.id,
User.email,
User.name,
User.created_at,
User.last_login
).all()
return jsonify([{
'id': u.id,
'email': u.email,
'name': u.name,
'created_at': u.created_at.isoformat(),
'last_login': u.last_login.isoformat() if u.last_login else None
} for u in users])
2. XSS (Cross-Site Scripting)
// ❌ Vulnerable to XSS
app.get('/search', (req, res) => {
const query = req.query.q;
res.send(`<h1>Results for: ${query}</h1>`);
// User input: <script>alert('XSS')</script>
// ✅ Properly escaped
import DOMPurify from 'isomorphic-dompurify';
import { escape } from 'html-escaper';
app.get('/search', (req, res) => {
const query = req.query.q || '';
// Sanitize and escape user input
const sanitized = DOMPurify.sanitize(query, {
ALLOWED_TAGS: [], // No HTML allowed
ALLOWED_ATTR: []
});
const escaped = escape(sanitized);
// Use template with escaped data
res.send(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'">
<title>Search Results</title>
</head>
<body>
<h1>Results for: ${escaped}</h1>
</body>
</html>
`);
});
3. Insecure Direct Object References (IDOR)
# ❌ Anyone can access any file
get '/files/:id' do
file = File.find(params[:id])
send_file file.path
end
# ✅ Proper authorization
get '/files/:id' do
# Authenticate user
halt 401, 'Unauthorized' unless current_user
# Find file
file = File.find_by(id: params[:id])
halt 404, 'File not found' unless file
# Check ownership or permissions
unless file.user_id == current_user.id || current_user.admin?
halt 403, 'Forbidden'
end
# Check file exists and is safe
unless File.exist?(file.path) && file.path.start_with?(Rails.root.join('uploads'))
halt 404, 'File not found'
end
# Log access
FileAccessLog.create(
file_id: file.id,
user_id: current_user.id,
ip_address: request.ip,
accessed_at: Time.now
)
# Send with proper headers
send_file file.path,
filename: file.original_filename,
type: file.content_type,
disposition: 'attachment' # Force download, don't execute
end
How to Properly Test AI Output
Here's my battle-tested process for validating AI-generated code:
The 5-Layer Testing Pyramid
┌─────────────────┐
│ Manual Review │ ← You read and understand it
├─────────────────┤
│ E2E Tests │ ← Does it work end-to-end?
├─────────────────┤
│ Integration │ ← Does it play nice with others?
├─────────────────┤
│ Unit Tests │ ← Does each piece work?
├─────────────────┤
│ Static Analysis│ ← Linters, type checkers, SAST
└─────────────────┘
1. Static Analysis (Automated)
Static analysis tools catch common issues before runtime. They're your first defense against AI-generated bugs:
# JavaScript/TypeScript
npm run lint # ESLint - catches syntax errors, style issues
npm run type-check # TypeScript - catches type mismatches
npm audit # Find vulnerable dependencies
npx madge --circular # Detect circular dependencies that cause issues
# Python
pylint your_module/ # Code quality and errors
mypy your_module/ # Type checking
bandit -r your_module/ # Find common security issues
safety check # Check dependencies for known vulnerabilities
# Go
go vet ./... # Catches common Go mistakes
staticcheck ./... # Advanced Go linting
gosec ./... # Go security scanner
Pro tip: Make these checks mandatory in your CI/CD pipeline. If AI code doesn't pass linting and type checking, reject it automatically.
2. Unit Tests (Required)
// Test the AI-generated function thoroughly
import { processPayment } from './payment';
describe('processPayment', () => {
it('should process valid payment', async () => {
const result = await processPayment({
amount: 1000,
currency: 'USD',
token: 'tok_valid'
});
expect(result.success).toBe(true);
expect(result.chargeId).toBeDefined();
});
it('should reject negative amounts', async () => {
await expect(
processPayment({ amount: -100, currency: 'USD', token: 'tok_valid' })
).rejects.toThrow('Amount must be positive');
});
it('should handle network errors', async () => {
// Mock network failure
jest.spyOn(paymentGateway, 'charge').mockRejectedValue(new NetworkError());
const result = await processPayment({
amount: 1000,
currency: 'USD',
token: 'tok_valid'
});
expect(result.success).toBe(false);
expect(result.error).toContain('network');
});
it('should not charge twice on retry', async () => {
const chargeSpy = jest.spyOn(paymentGateway, 'charge');
await processPayment({
amount: 1000,
currency: 'USD',
token: 'tok_valid'
});
// Verify idempotency key was used
expect(chargeSpy).toHaveBeenCalledWith(
expect.objectContaining({
idempotency_key: expect.any(String)
})
);
});
});
3. Security Testing
# Run security scanners
npm audit fix
snyk test
# OWASP ZAP for web apps
# SonarQube for code quality and security
# Manual security checklist:
AI Code Security Checklist:
- [ ] All inputs are validated
- [ ] SQL queries use parameterization
- [ ] No secrets in code
- [ ] Authentication is implemented correctly
- [ ] Authorization checks are present
- [ ] Rate limiting is in place
- [ ] CORS is configured properly
- [ ] XSS protection is enabled
- [ ] CSRF tokens are used
- [ ] Error messages don't leak information
- [ ] Logging doesn't include sensitive data
- [ ] Dependencies are up to date
4. Performance Testing
// Load test AI-generated endpoints
import autocannon from 'autocannon';
async function loadTest() {
const result = await autocannon({
url: 'http://localhost:3000/api/users',
connections: 100,
duration: 30,
headers: {
'Authorization': 'Bearer test-token'
}
});
console.log(result);
// Check results
if (result.errors > 0) {
console.error(`❌ ${result.errors} errors occurred`);
}
if (result.timeouts > 0) {
console.error(`❌ ${result.timeouts} timeouts occurred`);
}
if (result.latency.p99 > 1000) {
console.warn(`⚠️ 99th percentile latency: ${result.latency.p99}ms`);
}
}
Real Horror Stories (And Lessons Learned)
Story 1: The Infinite Loop of Doom
What happened: AI generated a retry mechanism that never gave up.
// AI generated this
async function fetchData(url) {
while (true) {
try {
return await fetch(url);
} catch (error) {
continue; // Try again immediately, forever!
}
}
}
Result: The server made millions of requests to a third-party API in a short period, causing service degradation and unexpected costs.
Lesson: Always set maximum retry limits and use exponential backoff.
// Fixed version
async function fetchData(url, options = {}) {
const {
maxRetries = 3,
baseDelay = 1000,
maxDelay = 30000,
timeout = 10000
} = options;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
const response = await fetch(url, {
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok && response.status >= 500) {
throw new Error(`Server error: ${response.status}`);
}
return response;
} catch (error) {
if (attempt === maxRetries - 1) {
throw error;
}
// Exponential backoff with jitter
const delay = Math.min(
baseDelay * Math.pow(2, attempt) + Math.random() * 1000,
maxDelay
);
console.log(`Retry ${attempt + 1}/${maxRetries} after ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
Story 2: The Password Leak
What happened: AI-generated logging that included sensitive data.
# AI thought this was helpful
def login(email, password):
logger.info(f"Login attempt: email={email}, password={password}")
# ... rest of login logic
Result: User passwords exposed in plaintext logs—a serious security vulnerability and potential compliance violation.
Lesson: Never log sensitive data. Use structured logging with filtering.
import logging
from typing import Dict, Any
class SensitiveDataFilter(logging.Filter):
"""Filter out sensitive data from logs."""
SENSITIVE_KEYS = {
'password', 'token', 'secret', 'api_key',
'credit_card', 'ssn', 'private_key'
}
def filter(self, record: logging.LogRecord) -> bool:
if hasattr(record, 'msg') and isinstance(record.msg, dict):
record.msg = self._redact_dict(record.msg)
return True
def _redact_dict(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""Redact sensitive keys from dictionary."""
redacted = {}
for key, value in data.items():
if any(sensitive in key.lower() for sensitive in self.SENSITIVE_KEYS):
redacted[key] = '[REDACTED]'
elif isinstance(value, dict):
redacted[key] = self._redact_dict(value)
else:
redacted[key] = value
return redacted
# Setup logger with filter
logger = logging.getLogger(__name__)
logger.addFilter(SensitiveDataFilter())
def login(email: str, password: str) -> Dict[str, Any]:
"""Authenticate user."""
logger.info({
'event': 'login_attempt',
'email': email,
'ip': request.remote_addr,
'timestamp': datetime.utcnow().isoformat()
})
# Password is NOT logged
# ... authentication logic
Story 3: The Race Condition
What happened: AI generated code that worked fine in development but failed in production.
// AI-generated code (looks fine!)
var counter int
func incrementCounter() {
counter++
}
Result: Lost transactions and data inconsistencies due to race conditions under high concurrency.
Lesson: Always consider concurrency. Use proper synchronization.
// Fixed version
import "sync"
type SafeCounter struct {
mu sync.RWMutex
value int64
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
func (c *SafeCounter) Value() int64 {
c.mu.RLock()
defer c.mu.RUnlock()
return c.value
}
// Or use atomic operations for simple cases
import "sync/atomic"
var counter int64
func incrementCounter() {
atomic.AddInt64(&counter, 1)
}
The AI Code Review Checklist
Use this checklist for every piece of AI-generated code:
Before Accepting AI Code
## Security
- [ ] Input validation on all external data
- [ ] No SQL injection vulnerabilities
- [ ] No XSS vulnerabilities
- [ ] Authentication and authorization present
- [ ] No secrets or credentials in code
- [ ] Rate limiting implemented
- [ ] Error messages don't leak sensitive info
## Error Handling
- [ ] All errors are caught and handled
- [ ] Errors are logged appropriately
- [ ] User-friendly error messages
- [ ] Retry logic has maximum attempts
- [ ] Timeouts are set for external calls
## Performance
- [ ] No N+1 query problems
- [ ] Database queries use indexes
- [ ] Pagination for large datasets
- [ ] Connection pooling configured
- [ ] Caching strategy in place
- [ ] Resource cleanup (files, connections)
## Testing
- [ ] Unit tests written and passing
- [ ] Edge cases covered
- [ ] Error cases tested
- [ ] Integration tests for external dependencies
- [ ] Performance tested under load
## Code Quality
- [ ] Code follows project conventions
- [ ] No magic numbers or hardcoded values
- [ ] Proper logging and monitoring
- [ ] Documentation/comments for complex logic
- [ ] Type safety (if applicable)
- [ ] Linter passes with no warnings
## Production Readiness
- [ ] Configuration externalized
- [ ] Environment variables used
- [ ] Graceful degradation implemented
- [ ] Monitoring and alerting considered
- [ ] Rollback plan exists
Building Better Prompts for Better Code
The quality of AI output is directly proportional to the quality of your prompts.
❌ Bad Prompt
"Create a function to handle user registration"
✅ Good Prompt
Create a secure user registration function in Node.js with Express that:
Requirements:
- Accepts email, password, and name
- Validates email format and password strength (min 8 chars, uppercase, lowercase, number, special char)
- Checks if email already exists in PostgreSQL database
- Hashes password with bcrypt (12 rounds)
- Stores user with created_at timestamp
- Returns user object without password
- Includes comprehensive error handling
- Uses Joi for validation
- Implements rate limiting (5 registrations per hour per IP)
- Logs registration attempts without sensitive data
- Uses parameterized queries to prevent SQL injection
- Returns appropriate HTTP status codes
Technical constraints:
- Use async/await
- Use TypeScript with proper types
- Include JSDoc comments
- Follow Airbnb style guide
- Include error logging with Winston
Please also include:
- Unit tests with Jest
- Example usage
- Error handling for all edge cases
Pro Tips for AI Prompts
- Specify the language and framework
- List security requirements explicitly
- Request error handling
- Ask for tests
- Mention performance considerations
- Specify coding standards
- Request logging and monitoring
- Ask for documentation
Conclusion: AI is a Tool, Not a Replacement
AI code generation is incredibly powerful, but it's not magic. Think of it like a junior developer who:
✅ Writes code quickly
✅ Knows many patterns
✅ Can handle boilerplate
❌ Doesn't understand your business logic
❌ Doesn't consider security by default
❌ Doesn't think about edge cases
❌ Doesn't test thoroughly
The Golden Rule: Never ship AI-generated code without:
- Understanding what it does
- Testing it thoroughly
- Reviewing it for security issues
- Adding proper error handling
- Ensuring it meets production standards
The Importance of Proper Review
The lesson is clear: blindly trusting AI output leads to problems. Here are common issues I've observed:
- Performance degradation - Missing pagination or inefficient queries
- Security vulnerabilities - Exposed sensitive data or weak authentication
- Resource exhaustion - Infinite loops or uncontrolled retries
- Data inconsistencies - Race conditions or missing validations
These issues aren't AI's fault—they happen when developers skip proper review and testing, treating AI as an infallible oracle instead of a helpful assistant that requires oversight.
The real cost isn't immediate—it's long-term. When systems fail because of inadequately reviewed code, it impacts reliability, user trust, and team confidence in the development process.
Use AI to accelerate development, but always combine it with thorough review, testing, and validation. The time invested in proper code review prevents much larger problems down the road.
Your Turn
I've shared my painful lessons so you don't have to learn them the hard way. Now I want to hear from you:
💬 What's your worst AI-generated code horror story?
🔧 What's your process for reviewing AI code?
💡 What security issues have you found in AI output?
Drop a comment below! Let's learn from each other's mistakes.
Additional Resources
- OWASP Top 10 - Security risks to watch for
- Conventional Commits - Better commit messages
- Testing Best Practices
- Node.js Security Checklist
Found this helpful? Follow me for more real-world development insights, security tips, and lessons learned from experience!
Top comments (0)