Five AI-Generated Frontend Bugs That Ship to Production and How to Catch Them
Problem Scenarios
"I used Cursor to generate a React component. It ran fine locally, but threw errors in production. After investigating for ages, I found the AI had written the dependency array for useEffect incorrectly, causing an infinite loop."
"Copilot helped me write a table component. It looked fine, but then customers reported the table wasn't updating its data…"
If you're using AI to assist with writing frontend code, you've likely encountered similar "AI hallucination bugs." This article compiles five of the most common pitfalls and how to quickly locate and fix them.
Pitfall 1: Missing Dependencies in React useEffect
Symptoms
AI-generated code:
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setUser);
}, []); // ⚠️ AI missed userId
The page loads correctly the first time, but the data doesn't change when switching users. In React Strict Mode during development, it runs twice, and the problem surfaces in production.
Cause
AI often treats [] as the default way to "run only once," ignoring external dependencies. This is especially common when the prompt only mentions "fetch user data" without specifying "re-fetch when userId changes."
Fix
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setUser);
}, [userId]); // ✅ Added userId
Advanced approach: Replace raw useEffect with useData or react-query. AI can also easily generate request logic with caching, which is safer than manually managing dependencies.
Pitfall 2: CSS Class Name Conflicts (Mixing Tailwind and Custom Styles)
Symptoms
AI-generated component:
<div className="flex items-center card-container">
Tailwind's flex items-center works fine, but the CSS Module class name card-container isn't automatically injected—because the AI wrote the CSS Modules styles.cardContainer as a plain string.
Cause
The AI mixed three styling approaches—global CSS, Tailwind, and CSS Modules—in a single component without following the project's conventions. It inferred "this should work" based on various code it has seen, but in a CSS Modules project, className="card-container" won't be transformed into a hashed class name.
Fix
// ✅ Unify using CSS Modules
import styles from './UserProfile.module.css';
<div className={`${styles.cardContainer} flex items-center`}>
General principle:
- Project uses Tailwind → Tell the AI "Use only Tailwind, do not write custom classes"
- Project uses CSS Modules →
className={styles.xxx} - Project uses CSS-in-JS → Explicitly specify styled-components or Emotion
Pitfall 3: Abuse of TypeScript Type Assertions
Symptoms
When the AI encounters a type error, its favorite trick is as any:
const data = response.data as any; // 🤦 One bad apple spoils the whole bunch
const items = data.items.map((item: any) => ({
...item,
label: item.name ?? '',
}));
The entire block of code becomes unprotected by types. Anyone building on this code later will see IDE hints all turn to any, rendering TypeScript useless.
Cause
The AI's optimization target is "code that runs," not "type safety." When it detects a type error, it prioritizes the cheapest fix—as any is the cheapest, but also the most harmful.
Fix
// ✅ Correct approach: Define interfaces + type guards
interface ApiResponse<T> {
code: number;
data: T;
}
interface Item {
id: string;
name: string;
}
function isItemArray(data: unknown): data is Item[] {
return Array.isArray(data) && data.every(
item => typeof (item as Item).id === 'string'
);
}
// Usage
const result: ApiResponse<Item[]> = response.data;
if (!isItemArray(result.data)) throw new Error('Data format abnormal');
Prompting tip: Add "Strictly prohibit the use of as any; complete types must be defined" to your prompt.
Pitfall 4: State Updates Depending on Stale Values
Symptoms
An AI-written counter component:
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
for (let i = 0; i < 5; i++) {
setCount(count + i); // ⚠️ Each time is 0 + i
}
};
After clicking, count becomes 4, not the expected 0+1+2+3+4 = 10. This is because React's state updates are asynchronously batched; each setCount in the loop is based on the same count value.
Cause
The AI doesn't understand React's closure trap and batching mechanism for state updates. It writes what intuitively seems like "add i each time," but the actual execution is "five consecutive calls to setCount(0+i)."
Fix
// ✅ Use functional updates
const handleClick = () => {
for (let i = 0; i < 5; i++) {
setCount(prev => prev + i);
}
};
The same applies to useReducer and Vue's ref/reactive—AI often writes code that reads the latest value directly instead of using functional updates.
Pitfall 5: Error Handling — "As long as it doesn't throw, it's a success"
Symptoms
AI-generated API call code:
async function loadData() {
try {
const res = await fetch('/api/data');
const json = await res.json();
setData(json);
} catch (e) {
console.error(e);
}
}
Looks fine? The pitfall is in the middle: res.json() can throw a JSON parsing error, and network errors can be caught, but HTTP 400/500 errors will not trigger catch and will be treated as success!
Cause
The AI's "cognitive blind spot": HTTP error codes (4xx/5xx) do not cause fetch to throw an exception; only network disconnection does. The AI's training data contains a large number of simple fetch → then → catch patterns but rarely emphasizes the res.ok check.
Fix
async function loadData() {
try {
const res = await fetch('/api/data');
// ✅ Must manually check HTTP status
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
}
const json = await res.json();
// ✅ Then check business status code
if (json.code !== 0) {
throw new Error(json.message || 'Business exception');
}
setData(json.data);
} catch (e) {
// ✅ Give user-friendly feedback, not just log
toast.error(e instanceof Error ? e.message : 'Request failed, please retry');
}
}
Key Takeaways Summary
| Pitfall | AI's Typical Mistake | Fix | Preventable via Prompt |
|---|---|---|---|
| useEffect Dependencies | Missing dependencies or incorrect [] |
Complete dependencies / Switch to SWR | ✅ "Consider all reactive dependencies" |
| CSS Conflicts | Mixing multiple styling approaches | Unify conventions, use CSS Modules | ✅ "Strictly follow project CSS conventions" |
| Type Safety | Abuse of as any |
Define interfaces / Type guards | ✅ "Strictly prohibit use of any" |
| State Updates | Depending on stale values, closure traps | Functional updates / useReducer | Requires manual review |
| Error Handling | Only catching, not checking status codes | Check res.ok + status codes | ✅ "Handle 4xx/5xx" |
Core advice:
- AI-written code must be reviewed line by line, especially state management and asynchronous logic
- When writing prompts, explicitly constrain ("No any", "Check HTTP status codes")
- Use ESLint + TypeScript strict mode to automatically catch low-level errors
- AI excels at writing boilerplate code, but business logic and edge cases should be handled by you
AI is an accelerator, not a chauffeur. The more critical the moment, the more you need to keep your own hands on the wheel 🚗