Let me tell you about the three months I spent writing every line of code by hand. No Copilot. No ChatGPT. No AI autocomplete. Just me, my editor, and the docs.
It started because I kept running into the same frustrating problem: code that looked right but behaved wrong. AI-generated functions that passed a quick glance but had subtle issues — wrong error handling, misunderstood edge cases, dependencies I didn't actually need. I was shipping code I didn't fully understand, and it was catching up with me.
If that sounds familiar, here's what I learned and how you can fix the same problem without going full luddite.
The Root Cause: Comprehension Debt
We talk a lot about technical debt. But there's a newer, sneakier form I've started calling comprehension debt — the gap between the code in your repo and your understanding of what it actually does.
Every time you accept a suggestion without fully reading it, that gap widens. Every time you prompt an AI to "just make it work" and paste in the result, you're borrowing against your own understanding.
This isn't hypothetical. Here's a real pattern I caught in my own code:
// AI-generated: looks reasonable at first glance
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
return data;
} catch (error) {
console.error('Failed to fetch user:', error);
return null;
}
}
Spot the bug? fetch doesn't throw on HTTP errors. A 404 or 500 response happily resolves, and response.json() might throw on a non-JSON error page, but by then you've lost the actual status code. This is the kind of thing you catch when you write it yourself, because you're thinking through each line instead of scanning it.
// What I actually needed
async function fetchUserData(userId) {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
// Preserve the status for callers to handle appropriately
throw new Error(`User fetch failed: ${response.status}`);
}
return response.json();
}
Smaller, clearer, correct. No try-catch swallowing errors silently. No returning null that forces every caller to do null checks.
The Debugging Problem
Here's where comprehension debt really bites: debugging. When something breaks at 2 AM and you're staring at code you didn't write — code you don't understand — you're essentially debugging someone else's work. Except there's no "someone else" to ask.
I tracked my debugging sessions for a month before and after I went AI-free. The pattern was clear:
- With AI-generated code: Average debug time on unfamiliar sections was ~45 minutes. I'd often have to re-derive the logic from scratch.
- Hand-written code: Average debug time dropped to ~15 minutes. I could reason about the code because I'd made every decision in it.
Those numbers aren't scientific. Your mileage will vary. But the directional signal was strong enough that I changed how I work.
The Fix: A Graduated Approach
I'm not going to tell you to stop using AI tools. That ship has sailed, and honestly, they're genuinely useful. But here's the process I landed on after three months of hand-coding.
Step 1: Write the skeleton yourself
Always write the structure, the function signatures, the data flow. This is where your architectural thinking lives.
# Write this part yourself — it's YOUR design
class OrderProcessor:
def __init__(self, inventory_service, payment_gateway):
self.inventory = inventory_service
self.payment = payment_gateway
def process(self, order):
# Step 1: validate inventory
# Step 2: reserve items
# Step 3: charge payment
# Step 4: confirm order
# Each step needs rollback logic for the previous steps
pass
def _validate_inventory(self, items):
pass
def _reserve_items(self, items):
pass
def _charge_payment(self, order):
pass
Those comments aren't fluff. They're your thinking, captured. When you come back to debug this at 2 AM, you'll know exactly what each piece was supposed to do and why.
Step 2: Write critical paths by hand
Error handling, authentication logic, data validation, anything involving money or user data — write it yourself. These are the paths where bugs are most expensive and where understanding matters most.
Step 3: Use AI for the boring parts (but read every line)
Boilerplate serialization? Unit test scaffolding? CSS grid layouts you've written a hundred times? Let the AI help. But read every line before you commit it. If you can't explain what a line does, rewrite it until you can.
Step 4: Implement a personal code review rule
Before committing any AI-assisted code, I now do what I call the "explain it" test: I pick a random function and explain it out loud as if I'm in a code review. If I stumble, I rewrite that section.
You can automate a lighter version of this with a pre-commit hook:
#!/bin/bash
# .git/hooks/pre-commit
# Flags files with high AI-generation markers
# Check for common AI patterns: overly verbose variable names,
# unnecessary try-catch wrapping, redundant comments
FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(js|ts|py)$')
for file in $FILES; do
# Flag files with suspiciously many TODO/FIXME from paste-and-forget
COUNT=$(grep -c 'TODO\|FIXME\|HACK' "$file" 2>/dev/null || true)
if [ "$COUNT" -gt 5 ]; then
echo "WARNING: $file has $COUNT TODO/FIXME markers. Review before committing."
fi
done
It's a simple heuristic, not a silver bullet. But it's caught me a few times.
Prevention: Building the Habit
After my three-month experiment, here's what stuck:
- Morning warm-up: I spend the first 30 minutes of coding without any AI tools. Just me and the problem. It's like stretching before a run — it keeps the muscles from atrophying.
- New domain, no AI: When I'm learning a new library or language feature, I force myself to use the docs directly. AI summaries skip the nuance, and the nuance is where the real understanding lives.
- Review diffs, not files: When reviewing AI-generated code, I look at the diff against what I would have written. If the approaches diverge significantly, I dig into why.
- Keep a "things I learned" log: Every time I catch an issue in AI-generated code, I write down what was wrong and why. After a month, you start seeing patterns.
The Honest Tradeoff
Look, I'm faster with AI tools. Meaningfully faster, especially on greenfield work and boilerplate-heavy tasks. Going fully hand-written for three months cost me velocity.
But I also shipped fewer bugs. I spent less time debugging. I understood my codebase better. And when things broke, I fixed them faster.
The sweet spot isn't "always AI" or "never AI." It's knowing when to lean on the tool and when to lean on yourself. The three months taught me where that line is — and it's probably different for you. But if you're finding yourself staring at code you wrote last week and having no idea how it works, that's your signal. Scale back, write more by hand, rebuild the muscle.
Your future self, debugging at 2 AM, will thank you.
Top comments (1)
That “looks right but behaves wrong” pattern is real.
Have you noticed whether it’s always comprehension, or if the same prompt/code path can actually behave differently over time too?
Feels like sometimes it’s not just understanding the code — the behavior itself isn’t fully stable.