fixmyvibe.codes
Back to Blog

The 10 Most Common Bugs in AI-Generated Code

7 min read By FixMyVibe Team
bugs ai-code debugging

After auditing dozens of AI-generated codebases, patterns emerge. The same categories of bugs appear across Cursor, Bolt, Lovable, and v0 projects regardless of the tech stack. Here are the ten most common — what they look like, why they happen, and how to fix them.

1. Missing Error Boundaries Around Async Operations

AI tools generate clean async/await code for the happy path. The error case gets skipped.

// Bug: crashes the entire component if the fetch fails
async function loadDashboard() {
const data = await fetch('/api/dashboard').then(r => r.json());
renderDashboard(data);
}
// Fix: always handle the failure case
async function loadDashboard() {
try {
const response = await fetch('/api/dashboard');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
renderDashboard(data);
} catch (error) {
console.error('Dashboard load failed:', error);
renderErrorState('Unable to load dashboard. Please refresh.');
}
}

Why it matters: Network requests fail. APIs return unexpected status codes. Without error handling, one failed request can crash a page or leave users staring at an infinite spinner.

2. Missing Loading States

Related to the above: AI generates the success state and often skips the in-between.

// Bug: component renders with undefined data while fetching
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
return <div>{user.name}</div>; // Crashes: user is null on first render
}
// Fix: handle all three states (loading, success, error)
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetchUser(userId)
.then(setUser)
.catch(setError)
.finally(() => setLoading(false));
}, [userId]);
if (loading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
return <div>{user.name}</div>;
}

3. SQL Injection via String Concatenation

This one is genuinely dangerous. AI sometimes generates database queries by string interpolation:

// Bug: SQL injection vulnerability
async function searchUsers(searchTerm) {
const query = `SELECT * FROM users WHERE name LIKE '%${searchTerm}%'`;
return await db.query(query);
}
// A malicious user submits: '; DROP TABLE users; --
// The resulting query: SELECT * FROM users WHERE name LIKE '%'; DROP TABLE users; -- %'
// Fix: always use parameterised queries
async function searchUsers(searchTerm) {
const query = 'SELECT * FROM users WHERE name LIKE $1';
return await db.query(query, [`%${searchTerm}%`]);
}

Why it still happens: AI training data contains older tutorials that used string concatenation before parameterised queries became standard. The pattern leaks through.

4. API Keys Exposed in Frontend Code

One of the most common and most critical issues:

// Bug: API key visible to anyone who opens DevTools
const openai = new OpenAI({
apiKey: 'sk-proj-abc123...', // Hardcoded in frontend bundle
});
// Fix: API calls go through your backend; keys stay server-side
// Frontend calls your API:
const response = await fetch('/api/generate', {
method: 'POST',
body: JSON.stringify({ prompt }),
});
// Your server (never exposed to client):
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY, // Environment variable
});

The real risk: API keys in JavaScript bundles are fully public. Your dist/ folder contains them in plain text. Anyone can extract your Stripe key, OpenAI key, or AWS credentials and use them — at your expense.

5. Race Conditions in Concurrent Operations

// Bug: multiple async operations complete in unpredictable order
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
// If user types "ca" then "cat", both fetches run.
// "ca" results might arrive after "cat" results.
fetchSearch(query).then(setResults);
}, [query]);
}
// Fix: cancel stale requests with AbortController
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
const controller = new AbortController();
fetchSearch(query, { signal: controller.signal })
.then(setResults)
.catch(err => {
if (err.name !== 'AbortError') setError(err);
});
return () => controller.abort(); // Cancel on query change
}, [query]);
}

6. Missing Input Sanitisation on User Content

Any user-provided content that ends up rendered as HTML needs sanitisation:

// Bug: stored XSS vulnerability
function renderComment(comment) {
container.innerHTML = comment.text; // Executes any <script> tags
}
// Fix: sanitise before rendering
import DOMPurify from 'dompurify';
function renderComment(comment) {
container.innerHTML = DOMPurify.sanitize(comment.text);
}
// Or better: use textContent for plain text (no HTML allowed)
function renderComment(comment) {
container.textContent = comment.text; // Never executes scripts
}

7. No Pagination on Database Queries

// Bug: returns ALL rows — catastrophic at scale
async function getAllPosts() {
return await db.posts.findMany(); // Could be 500,000 rows
}
// Fix: always paginate
async function getPosts(page = 1, limit = 20) {
return await db.posts.findMany({
take: limit,
skip: (page - 1) * limit,
orderBy: { createdAt: 'desc' },
});
}

What happens at scale: At 1,000 records this query takes 50ms. At 100,000 records it takes 5 seconds and crashes your database connection. We’ve seen apps go from fast to unusable after their first viral moment — not because of traffic, but because of unpaginated queries.

8. Auth Tokens That Never Expire

// Bug: token valid forever — a stolen token is permanently valid
const token = jwt.sign({ userId: user.id }, SECRET_KEY);
// No expiresIn option — default is no expiry
// Fix: short-lived access tokens + refresh token rotation
const accessToken = jwt.sign(
{ userId: user.id },
SECRET_KEY,
{ expiresIn: '15m' } // Access token: short-lived
);
const refreshToken = jwt.sign(
{ userId: user.id },
REFRESH_SECRET,
{ expiresIn: '7d' } // Refresh token: rotated on use
);

9. console.log Left in Production Code

More common than you’d think — and more consequential:

// Bug: logs sensitive data to browser console
async function processPayment(card) {
console.log('Processing payment:', card); // Logs card number!
console.log('User data:', user); // Logs PII
const result = await stripe.charges.create({...});
console.log('Stripe result:', result); // Logs internal charge IDs
}

Why it matters: Console logs are visible to any user who opens DevTools. Logging payment data, user PII, or internal system details is a GDPR/PCI compliance violation. AI tools leave these in because they helped during development.

10. Missing CORS Headers (or Wrong CORS Configuration)

// Bug: either blocks legitimate requests or allows all origins
app.use(cors()); // Allows ALL origins — security risk for auth'd endpoints
// Also common: CORS so strict it blocks your own frontend
app.use(cors({ origin: 'http://localhost:3000' })); // Breaks in production
// Fix: explicit, environment-aware CORS
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || [];
app.use(cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
}));

Spotting These in Your Own Codebase

Most of these bugs don’t cause visible errors during development — they surface in production with real users. If your AI-generated app is going live, it’s worth a systematic check:

  1. Search for console.log — remove or replace with a proper logger
  2. Search for .innerHTML = — ensure content is sanitised
  3. Check all fetch / axios calls for error handling
  4. Look at every jwt.sign() call — does it have expiresIn?
  5. Check for any string interpolation in database queries

Or let us do it — we’ve seen all of these before.


Found any of these in your codebase? We can fix them — typically within a week, no lengthy retainer required.