Find Every Debugging Trail Marker
- 5 minutes read - 892 wordsIf you’ve ever watched me debug, you might think I’m moving slowly. That’s because I try hard to find every marker on the debugging trail. I believe this is one of the most valuable skills in debugging.
What do I mean by ‘marker’?
Let’s say our debugging session is a trail through the forest. ‘O’ is where we are, and ‘X’ is the summit of a mountain. At ‘O’, we know little. At ‘X’, we understand the bug and we’ve fixed it.
O————————X
The path is marked by spray-painted swatches on the trees. These are things we learned that we didn’t know on the way to fixing the bug. We’ll annotate these with small x’s, and call them trail markers.
O—x——–x–x—-x-x-X
Don’t skip the markers! Here’s an example of skipping several markers at once:
“Hey, I wonder if this is happening in
WidgetClass
? That sure causes a lot of bugs.”
This kind of well-meaning comment in a debugging session is a record scratch for me. I call it Try This debugging, because you’re trying things and hoping that they work. Instead, let’s talk about finding those trail markers.
Finding Trail Markers
Imagine we have a domain object called a “game plan”, showing on a page. We expect this item to be “enabled”, but instead it’s “disabled”. To make matters worse, it’s a Heisenbug; whenever we attempt to make a game plan that becomes, through means we don’t understand, disabled, we can’t, but throughout the day, it happens frequently.
We’ve read the frontend code, and it’s simple: sending a payload with an enabled game plan, and receiving from the server a disabled game plan. The server is our mystery box– we’re sending the right data, and it’s returning the wrong data.
This is our first marker. Barring something bizarre, whatever is wrong is
happening server-side, in the game plan create
controller action:
def create
game_plan = GamePlan.new(game_plan_params)
if game_plan.save!
GamePlanSyncer.run!(game_plan)
render_success(GamePlanSerializer.new(game_plan))
else
render_error
end
end
Where should we put our debugger? We know nothing, and we so could put it anywhere in this method. Let’s walk through it from top to bottom and start finding those trail markers.
The first line of the method initializes a game plan. Is this working? It
must be: the controller action is returning a persisted game plan. We
can look at the game_plan_params
method to confirm that it isn’t doing
anything strange like mutating the parameters. It’s not. This is our second marker.
Next, we have our if
statement. Is our condition true? It seems to be, as the
method is returning a persisted new game plan. If it weren’t, we’d get a 422
response code via the render_error
method. This is our third marker.
There isn’t much method left, and GamePlanSyncer
is starting to look fishy.
Remember that and check out render_success
, which serializes and returns the
object. It would be bizarre for a serializer to change the data it
receives. But, we could read its code to confirm it isn’t breaking our
expectations. We’ve done that; it isn’t; so now we have our fourth marker.
There’s one line of the method left:
GamePlanSyncer.run!(game_plan)
The code in this class is causing our issue. Time to open up
game_plan_syncer.rb
and investigate it.
To recap, we found four markers on our way to the syncer:
- Frontend code sending a correct game plan, and receiving an incorrect game plan
- A controller action initializing a valid game plan
- A controller action saving that game plan
- A controller action returning a successful response with an incorrect game plan
In a few moments of code reading, we’ve found four trail markers that lead right to our problem class. We don’t have to backtrack because we’ve read and understood the code leading to our syncer. We’ve blazed a trail that we can explain and anyone could follow. And we’ve reduced to a single class the surface area from which the bug could be ordinating. Pretty awesome!
What About Skipping Markers?
You might be tempted, watching this unfold, to jump to GamePlanSyncer
the
moment you read the controller action, or earlier. And if you’re fluent in the codebase, fine.
But initially, it’s just a guess! Guesses trick us because sometimes they end up being correct, which obscures that they are a bad strategy. They aren’t efficient long-term, they aren’t scientific, and they are frustrating to follow as an observer.
Guessing is like spotting what you think is the summit and leaving the trail to walk toward it. Sometimes you end up at the summit! Other times you end up in much rougher terrain, in a restricted area, or lost in the woods.
Slow Is Smooth
I mentioned at the beginning of the post that this process might appear slow. It’s only slow the first time you do it. After one or two trips through the API code, I know how actions are named, parameters are accessed, records are saved, and errors are handled. I can make some assumptions in future debugging sessions. I still wouldn’t jump to the syncer class in almost any situation, but I’d have a reassuring hunch that that’s where I was headed.
This is how you get fast at debugging. You learn to reason about what your codebase does, following a series of steps from your location to the summit. You eschew shortcuts for a strategy that works every time.