Office Hours — What patterns help you catch when LLMs take the happy path and miss edge cases or error handling in generated code?
A daily developer question about AI/LLMs, answered with a direct, opinionated take.
What patterns help you catch when LLMs take the happy path and miss edge cases or error handling in generated code?
LLMs generate code that works on the mainline. They’re trained on GitHub’s average case, not its disasters. The result is code that passes happy-path tests and fails silently or catastrophically in production when the real world shows up.
The Problem Is Structural, Not a Model Issue
Claude Opus and GPT-6 Astra are excellent at generating code that runs when nothing goes wrong. They’re terrible at spontaneously imagining what can go wrong. An LLM sees a database query and generates correct SQL. It doesn’t volunteer that the connection might time out, that the result set could be too large to fit in memory, or that concurrent writes could race. You have to ask. Better yet, you have to know to ask.
This isn’t a training data problem. It’s that LLMs optimize for producing plausible code quickly. Edge cases are verbose, context-hungry, and often domain-specific. The model takes the path of least surprise.
Concrete Patterns That Catch Omissions
Test the failure modes first. Before asking the LLM to generate the happy path, describe the three ways the feature could break—network timeout, missing permission, invalid input—and ask it to generate the handlers. This forces the model to think about boundaries before it optimizes for brevity. It’s counterintuitive but works because you’ve primed the context with the constraints you care about.
Example prompt:
Before writing the API client, list three failure scenarios:
1. Connection timeout after 5 seconds with no response
2. HTTP 403 with insufficient permissions
3. Malformed JSON in the response body
Now write a Python client that handles all three.
Each error case should log, retry with exponential backoff, or raise a custom exception.
Generate the tests alongside the code. Ask the LLM to write property-based tests using Hypothesis or QuickCheck. These tools explore edge cases systematically—off-by-one errors, empty collections, null values, type mismatches. The LLM often generates adequate property tests even when it doesn’t think about edge cases in the implementation. Running the tests immediately exposes what the main code missed.
Ask explicitly for error paths. Don’t say “write a CSV parser.” Say “write a CSV parser. List all ways parsing can fail (malformed quotes, wrong column count, Unicode errors, empty file, file too large). Add assertions and exceptions for each case.”
The LLM will still miss some, but you’ve shifted the default from “ignore errors” to “handle them.”
Diff against reference implementations. If you have working code in your codebase, show it to the LLM alongside the new requirement. “Here’s how we handle errors in our current database client. Apply the same error-handling patterns to this new feature.” Patterns spread; LLMs copy structure reliably.
What Still Slips Through
Retry logic is chronically broken in LLM-generated code. The model generates exponential backoff but forgets to add jitter, so thundering herds happen anyway. It adds retry limits but not timeout limits, so you hang forever retrying a dead service.
Resource cleanup is another graveyard. Database connections leak. File handles stay open. Temporary directories fill the disk. The LLM generates the happy path and assumes Python’s garbage collector will handle it, or doesn’t think about it at all.
Concurrency is maybe the worst offender. An LLM generates thread-safe code that isn’t. It uses locks inconsistently. It creates race conditions between checking and using a resource. Testing on a single-threaded machine doesn’t catch this; you need stress tests or actual concurrent load.
Systematic Approach
Build a checklist in your code review or CI that catches patterns the LLM tends to miss:
- All external I/O has explicit timeouts (no infinite waits).
- All error responses are logged with context, not silently swallowed.
- File and resource handles are in context managers or try-finally blocks.
- Retry logic includes jitter and max attempts, not just backoff.
- Concurrent code has clear locking discipline or uses thread-safe primitives.
- Parser/decoder code validates before using (not after).
Run the checklist as a linter rule if possible, or at least make it part of code review. When you find a miss, add a test case to your harness and ask the LLM to regenerate with that test passing. Over time, your codebase’s error-handling patterns leak into the model’s context, and it gets better at this specific codebase.
Why Scaling the Model Doesn’t Solve This
Frontier models are smarter at reasoning about edge cases when prompted. GPT-6 Astra will think harder if you ask it to think harder. But it still optimizes for speed and brevity by default. It’s not that it can’t handle edge cases. It’s that it won’t volunteer them without pressure.
The real lever is your prompt. Describe the constraints. List the ways it fails. Ask for tests. Show it examples from your codebase. Then verify with automated checks.
Bottom line: LLMs miss edge cases because they’re not prompted to find them. Flip your workflow from reviewing generated code for problems to specifying problems first, then asking the LLM to solve for them. Pair that with automated linting for known omission patterns, and you’ll catch most of what ships broken.
Question via Hacker News