d.
Blog

Grep finds the easy bugs. The ones that hurt hide in the flow

For a while my idea of a code audit was a sequence of greps. Search for the dangerous patterns, the force-unwraps, the blocking calls on the wrong thread, the usual smells. Fix what turns up. Declare it clean. It felt thorough. It produced a tidy list every time.

Then someone pushed back: every time you say it is done, the next look finds more. Your method is wrong.

They were right, and the reason is worth knowing.

Grep finds smells, not symptoms

Searching for patterns finds bugs that look like bugs. A force-unwrap that might crash. A call that should not be on the main thread. These are real, and easy, and grep is great at them.

But the bugs that actually hurt users rarely look like anything in isolation. They are emergent. The individual lines are all fine. The problem is what happens when they run together, in order, in a real session. No single line is a smell. The combination is a disaster.

The one that taught me this: a save function that, line by line, looked completely reasonable. Except all of those reasonable lines ran on the main thread, in sequence, and together they took several seconds, during which the loading spinner sat frozen and the user assumed the app had crashed. Nothing in that function would ever show up in a grep for "bad code." It was bad flow.

Walk the path the user walks

The fix was to stop searching and start tracing. Pick the highest-risk thing a user actually does, the cold launch, the save, the big irreversible action, and step through it one beat at a time. At each step, ask the questions grep cannot:

  • Does this block the main thread, so the UI freezes while it runs?
  • Is the state on screen consistent right now, or is it showing something stale while an async thing finishes?
  • Is there a race, where two things assume an order nobody guaranteed?

These are flow questions. They only make sense in sequence, with the user's experience in mind. You cannot pattern-match your way to them. You have to follow the path.

That single change in method, trace the flow instead of scanning for smells, turned up three real, user-facing bugs in one pass, after five "clean" grep passes had missed all three.

How I audit now

  • Grep is the last step, not the first. Use it as a cross-check after you understand the flows, not as the main tool.
  • List the flows that matter, the few things a user does that, if they break, ruin the experience, and audit those by walking them end to end.
  • At every step, ask what is happening on the main thread, what state is visible, and what is assumed about ordering. That is where the painful bugs live.
  • Distrust a tidy list. If your audit produced a clean, satisfying set of small fixes and nothing about the actual experience, you probably searched instead of traced.

A grep tells you the code contains a known bad shape. It cannot tell you the app freezes for three seconds on the most important tap. Only walking the flow does that, and the flow is the only thing the user ever feels.