No problem can withstand the assault of sustained thinking. - Voltaire
Every bug is a problem, and as our good friend Voltaire said hundreds of years ago, we can solve any problem with a good amount of thinking.
Thinking is required at every step of this debugging process, you must be thinking while doing your code readings, while taking notes, while placing your prints and reading their results. This must come naturally to you, you are a programmer, like me, and being thoughtful is (or should be) a core characteristic of us programmers.
Let´s stop being so philosophical about it and turn this into practical advice.
Assume your user or a the guy from QA tells about a bug in your program, they also tell you how to reproduce the bug and how critical it is. What do you do next? You put a break point at the beginning of the flow that is reported with a bug? No! I used to do that and it does not work so please don’t
Instead, ask yourself what could be causing it, imagine the input values and conditions needed for your code to fail like this, reproduce the error and see if it logs something, then think again. You will recognise what is happening very quickly, like if there is a value being changed or deleted before the code is trying to do something else with it or maybe the input of a function is invalid and you will also get your suspects.
You have an idea now about what is causing the error to show, what triggers it. What you will do next is a code reading with this in mind, take your notes and think again, this time you will most likely see the cause of the error, but if you do not, just keep thinking, reading code, taking notes, drawing the flow of the program until you can find it. It really should not take more than one or two passes of the code to figure it out
If the program is entirely yours, then you should know your code well enough to be able to find a bug just by thinking about the conditions that can make your code behave in the way the bug is making it behave, if is not yours then you have to read the complete code of the program (not the complete project or app of course just the function, process, module, library or whatever it is you are debugging.) to acquaint yourself to it and make drawings and notes of how it works. Then go back to thinking.
Now, lets quote someone else here because just thinking can make you feel like you are not really making progress, but that is a misconception and totally disagree with this quote in this scenario:
If you spend too much time thinking about a thing, you’ll never get it done. — Bruce Lee
This is wrong when talking about programming. If we were talking about our goals and desires in life we could use this quote as a positive idea, like just go for it and do it! But in this case, jumping into adding breakpoints and writing code before thinking about the issue will make you waste more time than you think and you will end up debugging the new bugs produced by the code used to fix the previous bug… ironically. Pro Tip: Keep track of the amount of bugs you have per feature and the amount of bugs you produce when fixing other bugs. When that number starts to decrease it means you are getting better.
And remember de first quote in this series of articles because it is the ground in which all three of them are based.
The most effective debugging tool is still careful thought, coupled with judiciously placed print statements. — Brian W. Kernighan, in the paper Unix for Beginners (1979) page 12.