Good IDEs with good static analysis, good unit test/integration test/e2e test coverage, a true understanding of the code base before changing it, all these avoid most bugs.
When the production hits a bug, good monitoring and logging can help root-causing. It is extremely difficult to debug and reproduce a bug in a distributed environment. Many bugs I have root-caused are purely based on monitoring and logging then show the proof with mind-execution of the code.
I don't want to claim this is superior to printf debugging, or whatever other technique. I just like seeing the code executing, and debugging some weird error can sometimes be an almost relaxing activity, as you just step through the code one by one and try to keep track of what's happening without needing to think more than 2 or 3 steps ahead.
That said, I'm also the guy who will happily put breakpoints into internal framework code (e.g. Spring Boot) so that I can understand why the framework doesn't do what it's supposed to do. Usually, I find the answer, although maybe in some cases it would have been more economical to use a workaround instead, but that would leave me dissatisfied...
I'm not a good programmer, but I can say very proudly that I've never used any debugger (except for assembly language, where the debugger is more like an interactive shell).
there is one notable exception to the no debugger rule: if you are learning/exploring a new code base on your dev machine and/or when writing tests [to cover legacy code]. but apart from that, unless your work is really special, you should not need a debugger.
Furthermore, in response to what one the siblings said about creating logging infrastructure, in my own debugging I tend to not find much value in keeping a bunch of logging around. For me keeping logging around boggs me down. Typically what I will do if I do printf debugging is I add logging statements, hunt down the bug and then commit the fix and the added printfs because they add context but then I immediately make another commit where I remove the printfs. This way I have them in the version history should I need them, while at the same time avoiding extraneous log entries at later time.
And really I’ve found that over time I do printf debugging less and less.
I attribute this to mainly four things:
1. A friend of mine that encouraged me to use an actual debugger
2. Watching Jonathan Blow program on his Twitch stream and observing how proficiently and fast he is able to get the information that he needs by using the debugger
3. CLion. As much as I like the commandline, I never found gdb with the commandline interface to be a suitable tool for me. Whereas with CLion I get a good debugger that makes visible to me most of the information that I need while I am debugging
4. Through years of programming I have learned to recognize many of the types of bugs that I produce. Also, with experience I have learned to write code that is easier to reason about and to debug as opposed to in the beginning where I was trying to be “too clever” for my own good with the code that I wrote.
I think another of the sibling comments put it well about the debugger being like a scalpel. Trying to step through a whole program and following what’s going on would be slow, confusing and ultimately error prone. In order to effectively use a debugger I think you need to have a hypothesis about what is causing the bug and then set very specific breakpoints that you make use of in order to figure out where things are going wrong. From your initial observations you can set more breakpoints at a more coarse level (shallower call stack levels) and use those to observe what leads up to things going wrong.
Made me feel less bad about it :)