DEV Community

Cover image for Why Your AI-Generated Code is Probably Garbage (And How to Fix It)
SATINATH MONDAL
SATINATH MONDAL

Posted on

Why Your AI-Generated Code is Probably Garbage (And How to Fix It)

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

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);
});
Enter fullscreen mode Exit fullscreen mode

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
    });
  }
});
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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' });
  }
});
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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] || []);
});
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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])
Enter fullscreen mode Exit fullscreen mode

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>
  `);
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
       └─────────────────┘
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
      })
    );
  });
});
Enter fullscreen mode Exit fullscreen mode

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:
Enter fullscreen mode Exit fullscreen mode

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`);
  }
}
Enter fullscreen mode Exit fullscreen mode

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!
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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));
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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++
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

✅ 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
Enter fullscreen mode Exit fullscreen mode

Pro Tips for AI Prompts

  1. Specify the language and framework
  2. List security requirements explicitly
  3. Request error handling
  4. Ask for tests
  5. Mention performance considerations
  6. Specify coding standards
  7. Request logging and monitoring
  8. 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:

  1. Understanding what it does
  2. Testing it thoroughly
  3. Reviewing it for security issues
  4. Adding proper error handling
  5. 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


Found this helpful? Follow me for more real-world development insights, security tips, and lessons learned from experience!

Top comments (0)