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 failsasync function loadDashboard() { const data = await fetch('/api/dashboard').then(r => r.json()); renderDashboard(data);}
// Fix: always handle the failure caseasync 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 fetchingfunction 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 vulnerabilityasync 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 queriesasync 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 DevToolsconst 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 orderfunction 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 AbortControllerfunction 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 vulnerabilityfunction renderComment(comment) { container.innerHTML = comment.text; // Executes any <script> tags}
// Fix: sanitise before renderingimport 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 scaleasync function getAllPosts() { return await db.posts.findMany(); // Could be 500,000 rows}
// Fix: always paginateasync 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 validconst token = jwt.sign({ userId: user.id }, SECRET_KEY);// No expiresIn option — default is no expiry
// Fix: short-lived access tokens + refresh token rotationconst 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 consoleasync 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 originsapp.use(cors()); // Allows ALL origins — security risk for auth'd endpoints
// Also common: CORS so strict it blocks your own frontendapp.use(cors({ origin: 'http://localhost:3000' })); // Breaks in production
// Fix: explicit, environment-aware CORSconst 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:
- Search for
console.log— remove or replace with a proper logger - Search for
.innerHTML =— ensure content is sanitised - Check all
fetch/axioscalls for error handling - Look at every
jwt.sign()call — does it haveexpiresIn? - 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.