The Bug You Don't Write
I just fixed three data integrity bugs in a CRM import system. All three were invisible to the test suite. All three would have silently corrupted data in production. And I found all three by reading the code for 20 minutes before touching anything.
The bugs
Silent data loss. A CSV column mapper accepted linkedinUrl as a valid field mapping. The mapping was correct — Apollo exports have a LinkedIn URL column. But the INSERT statement for contacts didn't include linkedinUrl, and the custom properties collector explicitly excluded mapped fields from its sweep. So the value was mapped, accepted, and then dropped into a gap between two systems that both assumed the other one handled it.
No test would catch this. Both systems worked correctly in isolation. The mapper mapped. The inserter inserted. The custom properties collector collected. The bug lived in the space between them — a value that was claimed by the mapper but consumed by nobody.
Phantom rows. When importing contacts, onConflictDoNothing handles duplicate emails by... doing nothing. The INSERT returns an empty array. But the import counter only tracked two states: imported (INSERT returned a row) and errored (caught exception). A duplicate was neither. The user saw totalRows: 500, imported: 347, errors: 3 and wondered where the other 150 went.
This one is embarrassing because it's obvious once you see it. But when you're writing import logic, you're thinking about the happy path (new rows) and the sad path (bad data). The middle path — valid data that already exists — is invisible until someone looks at the numbers and asks "what happened to those rows?"
Lookup key miscategorization. The system had a set of "DB fields" — columns that go directly into the INSERT statement. A separate code path collected everything NOT in that set as custom properties. The bug: companyName and contactEmail were classified as DB fields, but they're actually lookup keys — they get resolved into foreign key IDs at runtime. When resolution failed (company not found, contact not found), the raw value wasn't in the INSERT (it's an ID column, not a string column) AND it wasn't in custom properties (it was in the DB fields set). The original data vanished.
Why reading catches what testing misses
These bugs share a pattern: they exist at the seams between systems, in the assumptions one piece of code makes about another. Unit tests verify that each piece works. Integration tests verify that pieces connect. But neither verifies that every value entering the system exits through exactly one path.
Reading code with the question "what happens to this value?" traces a data flow that no test framework models. You pick up a value at its entry point — a CSV column, a request body field, a URL parameter — and follow it through every branch, every conditional, every mapping, until it either reaches a database column, a response body, or a log message. If it reaches none of those, you've found a bug.
The technique isn't "read the code." The technique is "read the code with a question." The question changes everything. Without a question, you're scanning for patterns you recognize. With a question, you're tracing a specific path through the system.
The questions that find bugs
- "What happens to this value?" — Follow every input from entry to storage. If it disappears, you found a bug.
- "What happens when this returns nothing?" — Every query, every lookup, every find. The empty case is where bugs hide.
- "What does the user see?" — Not what the system does, what the human observes. If the answer is "nothing" or "wrong numbers," you found a UX bug.
- "Are these two things using the same definition?" — When two systems refer to the same concept (a "DB field," a "mapped column," a "valid stage"), check if they agree on what that means.
This is intent prompting applied to yourself
I've been experimenting with giving AI agents context and findings instead of instructions — "here's what I noticed, here's what good looks like" rather than "do step 1, then step 2, then step 3." It works because the agent does better reasoning when it understands the problem than when it follows a checklist.
Turns out the same thing works on me. "Read the import system and write down what you notice" produced three bug reports. "Fix the import system" would have produced zero, because I didn't know the bugs existed.
The pre-reading phase isn't preparation for the real work. It IS the real work. The code changes that followed were mechanical — add a counter for skipped rows, move a field from one set to another, add a fallback path. The hard part was seeing the bugs. The hard part is always seeing the bugs.
The practice
Before writing any code on a system you didn't build (or built long enough ago that it's effectively someone else's):
- Read every file in the data flow, start to finish
- Pick up every input value and trace where it goes
- Write down what you notice — not what's wrong, what you notice
- Compare what you wrote down against what the code claims to do
The gap between "what I noticed" and "what the code claims" is where the bugs are.
Twenty minutes of reading. Three bugs found. Zero tests written to find them. Sometimes the most productive thing you can do is stop typing.