Debugging is like being the detective in a crime movie where you are also the murderer.
– Filipe Fortes
this is a review of article i shared on dev.to some time ago on dev.to
How it started: a false illusion Link to heading
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
“Wow I can troubleshoot without adding print statements code!! I will be faster than before!”
How is going Link to heading
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 ? Link to heading
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 a debugger is like using a GPS for your car. It usually helps you, but sometimes it doesn’t work, and when that happens, you can feel completely lost because you don’t know the way.
Jumping to the debugger too soon prevents you from learning the code and improving your reading and reverse engineering skills. These skills can only be acquired by reading more and more code written by others. The more you do it, the better you become at understanding it.
Remember, there are situations where you cannot rely on the debugger, so it is better to have an alternative.
2. Manual actions
Inspecting many lines of code and variables requires manual and repetitive actions like setting breakpoints, stepping into functions, going to the next line, inspecting variables, adding watches, etc.
Manual actions are much slower than automatic actions performed by computers.
Keeping track of many variable values and the application state requires a 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
}
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)
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
A sample case: microservice with many data transformations Link to heading
Imagine a service exposing some functionality to the user that need to call other services to fulfill the request
In complex cases there are many data adaptation between the controller and the call to the other services
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
- Identify the start and end of code section to check
- Automatize checks by writing probes functions ( Example add function that print if a data structure contains the data that i am looking for or temporary kotlin extension functions on that structure)
- Put probes in code and create a git patch for local changes
- Run the application and check the probes
- Repeat step 3 until issue is found ( A binary search algorithm can limit the tries )
- Rollback local changes when done
In complex scenario i find this approach faster , incremental and it require less cognitive load. I just run the software check the logs , then restric the scope with binary algorithm and repeat. No long debug pauses , no multiple variables to watch to keep in head.
Issue is found now what? Link to heading
Regardless how i found my preference is:
- highlight the issue by writing a new test and watching it fail ***
- Fix the text
Conclusion Link to heading
When we have to find issues in code we have many option on the approach 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