Quick Answer
AI coding tools often leave TODO comments, empty function stubs, and placeholder implementations in production code. 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.
Incomplete implementations are a common source of production bugs in inherited projects. AI-generated projects are especially prone to this: the codebase looks complete because it compiles and runs, but entire feature branches are hollow.
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: VibeDoctor's Vibe Check flags throw new Error("not implemented") stubs, // TODO: implement and // FIXME: implement comments, and return null; // TODO placeholders (QUA-012), with the file path and line number. Hardcoded returns and skipped checks without those markers still need the searches above. 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 pending test.
A test named should process refund via Stripe marked with test.todo shows up in the summary of every Jest or Vitest run, so it will be found by any audit. A TODO comment in a source file is easily skipped in code review, and only tools you set up for it (such as the ESLint rule below) will notice it.
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 make stubs less likely.
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.