You prompted an AI, it generated a feature, you tested it, and it works. Ship it, right? But six weeks later, you need to modify that feature, and you realize you have no idea what half the code is doing or why it was written that way.
AI-generated code has a unique quality problem, it looks professional on the surface while hiding structural issues underneath.
The "it works" trap
When you write code by hand, you build a mental model as you go. When AI writes code, you skip all of that. The code appears fully formed, and your brain treats it as "done" because it runs.
// AI generated this for a shopping cart
function calculateTotal(items) {
let total = 0;
for (let i = 0; i < items.length; i++) {
if (items[i].type === 'physical') {
total += items[i].price * items[i].quantity;
if (items[i].price * items[i].quantity > 100) {
total -= items[i].price * items[i].quantity * 0.1;
}
} else if (items[i].type === 'digital') {
total += items[i].price * items[i].quantity;
if (items[i].price * items[i].quantity > 50) {
total -= items[i].price * items[i].quantity * 0.15;
}
} else if (items[i].type === 'subscription') {
total += items[i].price;
}
}
if (total > 200) {
total -= total * 0.05;
}
return Math.round(total * 100) / 100;
}This works. But look again: magic numbers everywhere (100, 0.1, 50, 0.15, 200, 0.05), items[i].price * items[i].quantity repeated six times, and discount logic buried inside the loop. When your product manager says "change the bulk discount threshold from $100 to $75," you have to read the entire function to find the right number.
Common quality issues in AI code
AI produces the same categories of problems repeatedly. Once you recognize them, you can spot them in seconds.
Over-abstraction
You ask for a function that sends an email, and AI gives you a configurable notification system with a strategy pattern and a factory.
// You asked: "send a welcome email to new users"
// AI generated:
class NotificationFactory {
static create(type, config) {
switch (type) {
case 'email':
return new EmailNotification(config);
case 'sms':
return new SmsNotification(config);
case 'push':
return new PushNotification(config);
default:
throw new Error(`Unknown notification type: ${type}`);
}
}
}
// What you actually needed:
async function sendWelcomeEmail(user) {
await emailClient.send({
to: user.email,
subject: 'Welcome!',
template: 'welcome',
data: { name: user.name }
});
}The factory pattern is valid, for a project that actually sends SMS and push notifications. If your app only sends emails, you inherited three classes you will never use.
Copy-paste patterns
When you ask AI to generate similar functionality multiple times, it does not remember what it already created. You end up with slightly different versions of the same logic scattered across your codebase.
// In userController.js - AI generated this
function formatDate(date) {
const d = new Date(date);
return `${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}`;
}
// In orderController.js - AI generated this separately
function formatOrderDate(dateStr) {
const date = new Date(dateStr);
const month = date.getMonth() + 1;
const day = date.getDate();
const year = date.getFullYear();
return `${month}/${day}/${year}`;
}
// In reportService.js - AI generated yet another version
const getFormattedDate = (d) => {
const date = new Date(d);
return [date.getMonth() + 1, date.getDate(), date.getFullYear()].join('/');
};Three functions doing the exact same thing. When you need to switch to ISO format, you have to find and update all three.
Inconsistent style
AI does not maintain a consistent code style across separate prompts.
| Pattern | File A (AI response 1) | File B (AI response 2) |
|---|---|---|
| Async handling | async/await | .then().catch() |
| Variable declarations | const everywhere | Mix of let and const |
| String formatting | Template literals | String concatenation |
| Error handling | try/catch | .catch() callback |
| Exports | Named exports | Default exports |
None of these are wrong individually, but mixing them makes the codebase feel like it was written by five different people.
Magic numbers and strings
AI generates literal values directly in the code with no knowledge of your configuration system.
// AI-generated auth middleware
if (token.exp < Date.now() / 1000 + 300) {
// what is 300? seconds? milliseconds? why this value?
}
if (attempts > 5) {
// is 5 the right number? where did it come from?
}
if (user.role === 'admin' || user.role === 'super-admin') {
// what if you add a new role?
}Technical debtWhat is technical debt?Shortcuts or compromises in code that save time now but create extra work later when you need to change or extend it. accumulates faster with AI
A developer writing code by hand might produce 200 lines per day and accumulate debt slowly. With AI, you can generate 2,000 lines per day, and accumulate ten times the debt. More code means more surface area for bugs, more duplication, and more diverging patterns.
This is not a reason to stop using AI. It is a reason to budget time for refactoringWhat is refactoring?Restructuring existing code to make it cleaner, more readable, or more efficient without changing what it does.. The developers who ship fastest with AI generate code in bursts, then clean up before moving on.
When to refactor vs when to leave it alone
Not everything needs refactoringWhat is refactoring?Restructuring existing code to make it cleaner, more readable, or more efficient without changing what it does.. Here is a practical framework.
| Situation | Action | Why |
|---|---|---|
| Code you will never touch again | Leave it | Refactoring has no future payoff |
| Prototype or throwaway code | Leave it | It will be deleted anyway |
| Code you are about to extend | Refactor first | Cleaner code is easier to modify safely |
| Code with duplicated logic | Refactor | Duplication leads to inconsistent behavior |
| Code others will read | Refactor | Readability is a team investment |
| Code with magic numbers in business logic | Refactor | You will forget what those numbers mean |
| Code that works and is isolated | Leave it | Do not fix what is not causing problems |
The pragmatic rule: refactor when the cost of not refactoring is higher than the cost of refactoring. If you are about to extend the pricing code and it is a mess, clean it up first. If the avatar component works and nobody is touching it, leave it.