The hidden trap of debugger

rmarioo

Mario Russo

Posted on December 14, 2021

The hidden trap of debugger

"Debugging is like being the detective in a crime movie where you are also the murderer." - Filipe Fortes

How it started: a false illusion

When i had my first programming experience i was not aware of the possibility of using a debugger.
It happened that i had to understand why my software was not working as expected , so the only way i had to understand what was going on was to add some print statements all around the code.
Later I learnt that i can use the debugger as a tool to inspect variables and understand the status of my software in a specific time.
I thought

"I can troubleshoot without adding new code!!
I will be faster than before!"

How is going

After years working on many and more complex projects i almost totally change my mind, and realised that actually the debugger slowed me down!
To be clear i still think that in few specific simple cases it is worth to leverage the debugger but in complex cases i think there are better alternatives.
What happened ?
I was thinking that debugger was the only way to troubleshoot issues so i used it all the times.
If i come back to the my most stressful working times almost all of them were moment in which i was trying to understand the code ,spending hours in long debugging sessions inspecting tons of line of code variables and stack trace.
Can you remember your most stressful working time ?
Can you remember if in those moment you were in long debugging sessions ?
Probably you were trying to escape the legacy code matrix and you were stuck on the first phase: the understanding phase

Where is the trap ?

I will share now why in my opinion in complex use cases using the debugger actually slow us down and increase our stress and cognitive load

1. Give up to learn the code
Using the debugger is like using a navigator for your car. It actually helps you but sometimes for some reason it dint work and when it happens you are totally lost because you don't know the path.
Jumping soon to debugger prevent you to learn the code and to improve your reading and reverse engineering skills.
Those are skill that you can acquire only reading more and more code written by someone else. The more you do the more you get better to understand it. An remember there are cases in which you cannot rely use the debugger so it is better to have an alternative.

2. Manual actions
Inspecting a lot of lines of code and variables require manual and very repetitive action like put breakpoint , enter inside function , go to next line , inspect variable, add watches etc..
Manual action is much slower then automatic action that could performed by computers.
Keeping in mind a lot of variable values and application state require high cognitive load and energy.

3. Repetitive actions
Imagine that you have the following type hierarchy

data class University(val departments: List<Department>)

data class Department(val courses: List<Course>)

data class Course(val students: List<Student>)

data class Student(val name: String, val country: Country)

enum class Country {
    ITALY,
    SPAIN,
    FRANCE
}
Enter fullscreen mode Exit fullscreen mode

and you need to check more than one time if an university has students from Italy
Every time you need to inspect the country of student in university you need to do at least 5 action like five clicks , inspect variable take notes or remember their values
If you have to it 10 times , you will have to do 50 click or debugger inspections!.

An alternative approach could be write a function like

fun logItalianPresenceInUniversity(university: University)
Enter fullscreen mode Exit fullscreen mode

that prints what you want to check. Few additional code to write and just 1 click to execute and check the result.

3. Lack of trust in tests
Someone said

"A bug is just a missing test"

The purpose of our tests is to help us to define the behaviour of our system and help us to find issues.
An alternative way to debugging to detect an issue is to reproduce it by adding a new test.
Now if we found a bug thanks to debugger we can call in trap to think "test suite did not help me, so i will do the fix and forget the tests".
So we may enter in this dangerous loop

debugger loop fix without tests

A sample case: microservice with many data transformations

Imagine a service exposing some functionality to the user that need to call other services to fulfil the request
In complex cases there are many data adaptation between the controller and the call to the other services

A service with many adaptation steps

Now let's suppose that there is a bug in that service and some information that arrives from the external service is not sent back to the controller and the user.
Your task is to spot it and fix it and you need to deep dive in many adaptation layers.
Which options you have ?

1: Understand and document every little detail of the code
The safest option is to read carefully the code trying to understand it , taking notes and spot the missing point.
This option is good but may require a lot of time and effort to deep dive in the code structure. In legacy systems or when you don't have much time it can become an endless process. On other hand it on long term it improve your skills to read the code.

2: Use the application with debugger
We could put a breakpoint on controller and on client and use the application.
In our case having a lot of adaptation layers this will be take a lot of time with debugging session and so big effort.
For non trivial cases this approach is probably the worst one.

3: Use the application with probes in code
Here you can guess what is the idea.
You can write some small probe functions to automatically check what would manually ask the debugger to do. Remember the logItalianPresenceInUniversity function described above ? .

My personal approach in this cases is
1. identify the start and end of code section to check
in this case the controller and the client
2. Automatize checks by writing probes functions
Example add function that print if a data structure contains the data that i am looking for
3. Put probes in code and create a git patch for local changes
4. Run the application and check the probes
5. Repeat step 3 until issue is found
A binary search algorithm can limit the tries
6. Rollback local changes when done

In complex scenario i find this approach faster , incremental and it require less cognitive load.

Issue is found now what?

Regardless how i found my preference is:

  1. highlight the issue by writing a new test and watching it fail***
  2. Fix the text

Conclusion

When we have to find issues in code we have many option on the strategy to follow.
Using the debugger is only one of them and in my opinion in some cases is not the best approach.
Having more than one option we can choose the one that fit more our case.

*** Writing a new test can be hard in some cases you may need to break dependencies to isolate the section of code as described in working effectively with legacy code

💖 💪 🙅 🚩
rmarioo
Mario Russo

Posted on December 14, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

The hidden trap of debugger
productivity The hidden trap of debugger

December 14, 2021