Not universally applicable, but another advantage of print based debugging is that it pushes the system towards being more observable and thus helps with debugging production issues. If you can’t debug it from the logs during development, what hope do you have of debugging a prod issue?
Or, looking at it from the other end, if you have to put in enough logging to debug prod issues anyway, then the need for a debugger largely disappears as a result.
A solid majority of the software I’ve been responsible for at my day job has been some kind of web server process that is breaking for a specific user in prod. There is no way for me to use a debugger to diagnose the problem if I first hear about it in the context of error alarms reporting that 500 responses are getting served to live customers. Print debugging is the only tool I have.
There’s a key advantage to trace-based debugging: it can capture flows, rather than point in time. In debugging clang, for example, I generally see a crash when walking the AST that is caused by an error in the code that created that AST node. By the time that the debugger attaches to the crash, all of the state that was associated with the incorrect code is gone, other than the output. The debugger can let me introspect a load of ephemeral state associated with the victim code, but that isn’t useful.
Print-based debugging is the simplest case of trace-based debugging, where the traces are unstructured and ad-hoc. Structured logging is better, but requires a bit more infrastructure.
The biggest value of tools like valgrind is not that they catch memory-safety bugs, it’s that they automatically correlate these bugs with temporally distant elements. When I have a use-after-free bug in a program that I run with valgrind, it tells me where the use is but it also tells me when the object was allocated and when it was freed. These two extra pieces of information make it incredibly easy to fix the bug in comparison to having only the location of the use of the dangling pointer.
Why not connect the debugger to prod?
Depends on the type of project I guess, but in my corner of the industry, that would be rather complicated due to the deployment and security model. Logs are much easier.
Can you guarantee the application won’t halt for everyone while you’re taking your time single-stepping through it?
I will preface this by saying it is always dangerous to perturb something by using atypical mechanisms (debugging apis eg ptrace, but also in many cases runtime flags that turn on rarely used instrumentation).
That said, don’t single step. Even outside of production threads or having a gui tends to make that awkward. Instead take advantage of programmable breakpoints and/or memory dumps. In the simplest form you can do printf debugging but with printfs that didn’t occur to you before the process was started.
Most operations are subject to timeouts that are immediately triggered by a debugger breakpoint. Which means every debugged operation basically immediately fails. Debuggers don’t really work in distributed systems, unfortunately.
I’d throw in 2 more dimensions of this problem. One is how much do you switch environments. I’m a frequent developer, but on an average day I’ll touch go, ruby, c, c#, nix. On a bad day, there will be many more. And I’m likely to choose whatever is the common denominator that works for me, which often is the print statement.
Another one is how easy is it to use the debugger when needed. If I do c#, it’s trivial. Whether I’m switching languages versions or projects, VS has the debugger immediately available. On the other hand I’m told nodejs can be debugged. I wasted enough time on that - if someone can set it up so that it works for me in vscode for any project with asdf and nix-shell I may use it one day. Slightly less annoying but also a barrier is ruby - got to google how to use pry every other month and it requires me to actually add the extra libraries to the app.
I got through a notoriously difficult C class during my CS degree using mostly just printf debugging. My classmates thought I was insane, but while its embarrassing to admit, using a C debugger is difficult and foreign for me.
When debugging memory/allocation issues I usually reach for valgrind.
Recently while doing some RE CTF challenges I’ve learnt to use GDB more confidently, but I always forget the commands. printf debugging is what I always initially do when I encounter a problem, not just in C but any language. I feel it’s just how my brain likes to work. And, like the article says, printing stuff is easy to do in every language.
Another axis, for me, project maturity. In firmware land with JTAG toys, debuggers tend to be used in the beginning of a project long before there’s anything that could be called a product. Somewhere in the life-cycle, the build is moved off a dev’s desk into some kind of CI/CD pipeline. Additionally, the hardware debug facility stops being populated to save some pennies on the BoM. So the team’s ability/willingness to support the IDE tends to atrophy. Fast forward years, original devs are long gone, debuggers no longer work because $reasons and the poor sod having to identify and fix a problem on customer premises rarely feels that resurrecting some random debugger tech is the fast path to success. Printing/logging for the win.
I vary depending on the environment
In my ruby cli apps, i like using the debugger, all i have to do is type binding.pry & i basically just remember that sometimes i have to put !!! to quit.
In frontend where i have the browser (ui) & hot reloading, printing is almost always good enough.
I think the right tool is generally what allows quickest iteration — aside from rare seriously complex bugs
I’m not sure what to make of this article when the author categorizes frequent and infrequent developers on the first hand when it comes to debugging.
I prefer simplicity and flexibility over specializing the IDE specific features. Print debugging makes me much easier to achieve that. Rather quicker to be honest for me when I see people work with me with debugger, I noticed that sometimes on fixing the bugs.
Don’t really understand the point unless I’m missing something. I would say it doesn’t necessarily need to be applicable for everyone.
Of course it doesn’t apply to everyone - but I read it like I do it myself. If I’m using a language/project every day or even every week I might have a debugger set up. If I look at this project for the first time, or something pops up twice a year, maybe not in one of my 2-3 main languages - no chance I’ll have a debugger set up.
So “do you work with this daily/regularly”, then you are a frequent developer :P (PS: Big fan of print debugging myself, but I use debuggers a lot, too)