Quick Answer
AI coding tools leave TODO comments, empty function stubs, and placeholder implementations in production code more often than any other class of issue. These are not reminders for later - they are missing features that users will encounter. Every TODO in a code path is a bug waiting to be filed. The fix is to treat TODOs as failing tests: they block deployment until resolved.
The TODO Problem Is Worse Than You Think
Every software project has TODO comments. What makes AI-generated codebases different is the density and placement of those TODOs. A human developer adds a TODO because they made a deliberate trade-off - ship fast, clean up later. An AI model adds TODOs because it ran out of context, hit a complexity boundary, or received a prompt that implied the feature without specifying it. The AI does not know it is leaving a production gap; it is generating a scaffold.
The result is TODOs in critical paths: payment processing functions that return hardcoded values, email sending stubs that log "email sent" without sending anything, authorization checks replaced with // TODO: verify user has permission.
According to Stack Overflow's 2023 Developer Survey, 47% of developers say incomplete implementations are the most common source of production bugs in projects they have inherited. For AI-generated projects, this number is anecdotally higher - the codebase looks complete because it compiles and runs, but entire feature branches are hollow.
SonarSource's analysis found that the average JavaScript codebase contains 1 TODO comment per 200 lines of code. In projects built primarily with AI tools, that ratio drops to 1 per 80 lines - 2.5x the industry baseline.
Three Types of AI-Generated Placeholders
Understanding the taxonomy of placeholder code helps you search for and prioritize fixes:
Type 1: The annotated stub - a function that exists but does nothing, with a TODO explaining what it should do.
Type 2: The hardcoded return - a function that returns a static value or empty array to make TypeScript happy, with no indication that real logic is missing.
Type 3: The skipped validation - an authorization or validation step replaced with a comment because the AI did not have enough context to implement the real logic.
// ❌ BAD - All three placeholder types in one file
// Type 1: Annotated stub
async function processRefund(orderId, amount) {
// TODO: implement refund via Stripe
// TODO: update order status to 'refunded'
// TODO: send confirmation email
console.log('processRefund called', orderId, amount);
}
// Type 2: Hardcoded return
async function getUserPermissions(userId) {
// TODO: fetch from database
return ['read', 'write']; // Returns hardcoded permissions for everyone
}
// Type 3: Skipped validation
async function deleteAccount(req, res) {
const { userId } = req.params;
// TODO: verify requesting user has admin role
await db.users.delete({ where: { id: userId } });
res.json({ success: true });
}
The third example is the most dangerous. The authorization check is missing, and any authenticated user can delete any account. The TODO makes it look like someone is aware of the issue, but it is already deployed and already exploitable.
What Complete Implementations Look Like
// ✅ GOOD - Every code path is implemented
async function processRefund(orderId, amount) {
const order = await db.orders.findUnique({ where: { id: orderId } });
if (!order) throw new Error(`Order ${orderId} not found`);
if (order.status === 'refunded') throw new Error(`Order ${orderId} already refunded`);
const refund = await stripe.refunds.create({
payment_intent: order.stripePaymentIntentId,
amount: Math.round(amount * 100),
});
await db.orders.update({
where: { id: orderId },
data: { status: 'refunded', stripeRefundId: refund.id },
});
await sendRefundConfirmationEmail(order.userEmail, orderId, amount);
return { refundId: refund.id };
}
async function getUserPermissions(userId) {
const userRole = await db.userRoles.findUnique({ where: { userId } });
if (!userRole) return [];
return ROLE_PERMISSIONS[userRole.role] ?? [];
}
async function deleteAccount(req, res) {
const requestingUser = req.user; // Set by auth middleware
const { userId } = req.params;
if (requestingUser.role !== 'admin' && requestingUser.id !== userId) {
return res.status(403).json({ error: 'Forbidden' });
}
await db.users.delete({ where: { id: userId } });
return res.json({ success: true });
}
How AI Models Generate Placeholder Code
Understanding why AI tools leave stubs helps you prompt more effectively to avoid them. There are four common mechanisms:
| Mechanism | What the AI does | How to prevent it |
|---|---|---|
| Context overflow | Hits token limit mid-function, adds TODO for the rest | Ask for smaller, focused functions; continue from the TODO in a follow-up prompt |
| Missing dependencies | Does not know your Stripe setup, leaves TODO | Provide existing code patterns in context: "we use stripe.paymentIntents.create" |
| Ambiguous requirements | Prompt says "add auth" but doesn't specify roles/logic | Specify exactly what the auth check should do in the prompt |
| Scaffold generation | Generates file structure with stubbed functions intentionally | Ask explicitly: "implement each function fully, no stubs" |
Finding Every TODO in Your Codebase
A manual code review will not catch all placeholder code. Hardcoded returns and skipped validations look like working code to a human reviewer moving quickly. Three approaches that work at scale:
Search patterns to run immediately:
// TODO,// FIXME,// HACK,// XXX- the four standard incomplete-code markersthrow new Error("not implemented")- a common AI stub patternreturn []orreturn nullimmediately after a function declaration - likely a hardcoded stubconsole.log("TODO")orconsole.warn("not implemented")- Functions whose entire body is a single
returnstatement returning a primitive - investigate each one
Add a CI check: A pre-commit hook or CI step that fails the build on any TODO: in critical directories (src/api/, src/services/, src/lib/) forces resolution before merging. This is a forcing function that turns TODOs into action items with a deadline.
Automated scanning: Tools like VibeDoctor (vibedoctor.io) automatically scan your codebase for incomplete implementations and TODO stubs, flagging specific file paths and line numbers. Free to sign up.
The Right Way to Use TODO Comments
TODOs are not inherently bad. They are bad when they are the only signal that a feature is missing. A better practice is to replace TODOs in production code with two things: a tracking issue and a failing test.
A failing test named should process refund via Stripe that is skipped with test.skip is visible in your test suite, counts against coverage, and will be found by any audit. A TODO comment in a source file is invisible to every automated system and easily skipped in code review.
For placeholders that must ship temporarily (an integration pending a third-party API key), wrap them in an explicit feature flag that returns an early error in production, not a silent no-op.
FAQ
How do I know if a TODO is in a critical code path?
Follow the call chain. If the function with a TODO is reachable from a route handler, a Supabase edge function, or a scheduled job - it is in a critical path. If it is only reachable from a test or a dev utility, it is lower priority. Start with TODOs that touch payment, authentication, or data mutation.
Should I use ESLint to ban TODOs?
Yes, selectively. The ESLint rule no-warning-comments can be configured to warn on TODO, FIXME, and HACK. Setting it to "error" in CI prevents new TODOs from merging. Existing TODOs can be tracked and resolved incrementally before setting the rule to error in non-critical files.
Cursor keeps generating stubs even when I ask for complete code. What helps?
Add this to your Cursor rules or system prompt: "Never use TODO comments or placeholder implementations. Every function body must be fully implemented. If you lack context to implement something, ask a clarifying question instead of generating a stub." Explicit constraints reduce stub generation significantly.
What is the difference between a stub and dead code?
A stub is a placeholder for code that is supposed to run - it is called but does nothing useful. Dead code is code that is never called - it exists but has no execution path leading to it. Both are problems, but stubs are more urgent because they actively fail users in production while appearing to work.
How many TODOs is too many?
Any TODO in a production code path is too many. TODOs in test utilities, dev scripts, or clearly-marked future enhancement sections are acceptable as long as they are tracked in an issue tracker. A codebase where TODOs accumulate in source files without linked issues is one where technical debt is invisible and unmanaged.