top | item 26925570

The unreasonable effectiveness of print debugging

349 points| goranmoomin | 4 years ago |buttondown.email | reply

354 comments

order
[+] dmitryminkovsky|4 years ago|reply
Whenever this comes up, I think of this quote from The Practice of Programming by Brian W. Kernighan and Rob Pike [0]:

> As personal choice, we tend not to use debuggers beyond getting a stack trace or the value of a variable or two. One reason is that it is easy to get lost in details of complicated data structures and control flow; we find stepping through a program less productive than thinking harder and adding output statements and self-checking code at critical places. Clicking over statements takes longer than scanning the output of judiciously-placed displays. It takes less time to decide where to put print statements than to single-step to the critical section of code, even assuming we know where that is. More important, debugging statements stay with the program; debugging sessions are transient.

I found this lines up with my personal experience. I used to lean on interactive debuggers a lot, and still enjoy using them. They're fun and make for good exploring. But the act of figuring out where you want to print really makes you think in ways that interactive debugging cannot. I find the two forms really complement each other.

[0] https://logging.apache.org/log4j/2.x/manual/index.html

[+] mumblemumble|4 years ago|reply
There's a part of me that wants to say that that opinion has to be taken with a grain of salt and a lump of paying attention to who is offering it.

Circa 1999, one would assume that Brian Kernighan and Rob Pike are largely drawing experience from working with C, which is a relatively verbose language. Single stepping through C code in a debugger is indeed a laborious process.

If you read accounts from Smalltalk developers, on the other hand, it's clear that they very nearly live their entire lives inside the debugger, and consider it to be an enormous productivity booster.

I would guess that there are several effects going on there. One would be that, in terms of how much effective work it accomplishes, a line of Smalltalk code is generally not equivalent to a line of C code. That has a big impact on just how many steps are involved in single-stepping your way through a region. The other is the nature of the debugger itself. Gdb and Smalltalk debuggers are wildly different pieces of software.

[+] ReactiveJelly|4 years ago|reply
> we find stepping through a program less productive than thinking harder and adding output statements and self-checking code at critical places.

Uh, me too. That's why I don't single-step through huge chunks of a program.

I use code breakpoints.

[+] zoomablemind|4 years ago|reply
In a way, the print-debugging is a sign of 'owning' the code, when a developer is very much familiar with the structure and internal works of the project. This is akin to surgeon precisely pointing the scope and scalpel.

Add to this a need to build a debugging-enabled version of the project - an often long-running process, compared to a few edits to some 'release' build.

On the other hand, when dealing with an unfamiliar or complex and well-forgotten project, debuggers become that discovery and exploration tool that offers a wider context and potentially better situational awareness.

Of course, mix-in some concurrency and either debugging approach can equally become cumbersome without proper understanding of the project.

[+] ab111111111|4 years ago|reply
I think the Key phrase from that quotation is "thinking harder". A debugger gives you all of the programme's state as a kind of vast animation, so it's easy to start working with one thinking "Something's going wrong here, so I'll just step through the whole programmme and see when things start looking like they're going wrong". It's then easy to miss the problem due to the vast quantity of data you have to parse. Using print statements, in contrast, forces you to formulate simple hypotheses and then verify or falsify them, e.g. "I think this variable in this function is causing the problem" or "I think everything's fine with the execution up to this point". I.e. the very fact that a debugger works so well at giving you an insight into how the programmes state can itself be part of the problem: it can be overwhelming.
[+] wonnage|4 years ago|reply
The Chrome devtools make adding a logpoint as easy as setting a breakpoint. So instead of adding your print statement, and rebuilding the app you can do it live. I think this is strictly better as you still get to enjoy the mental exercise of deciding where to put the logpoint. Even better, you can run anything you want in a logpoint, my favorite is console.profile/profileEnd() to get a cpu profile between two lines of code.
[+] neonological|4 years ago|reply
There's another methodology that Brian and Rob Pike miss.

Rather then stepping through a program. Add breakpoints and just step from breakpoint to breakpoint.

With a good IDE, adding a breakpoint and hitting a shortcut key is faster than a print statement and on the GUI IDE your debugging sessions are not transient.

The only time their advice makes sense to me is when I'm in an environment without a gui. Even then jetbrains has a "ssh full remote mode." However at my company I have found that this feature doesn't work under Nix (nix-shell) so we all just use print statements.

[+] CJefferson|4 years ago|reply
Personally, I think my biggest reason for using print debugging is.. it works.

In C++ I often find the debugger doesn't find symbols on projects built with configure/Make. If I have a Java Gradle project I have no idea how to get it into a debugger. Python debuggers always seem fragile. Rust requires I install and use a "rust-gdb" script -- except on my current machine that doesn't work and I don't know why.

I'm sure in each case I'm sure I could get a debugger working given enough time, but the only error I've ever had in print debugging is failing to flush output before a crash, and it's never been hard to search for "how to flush output" in whatever language I'm current using.

[+] pmichaud|4 years ago|reply
I think the point about seeing the state over time is a great one.

But also I want to nitpick because the title is one of my “favorite” pet peeves: “The Unreasonable Effectiveness of ...” thing is now used (as in this article) by people who are trying to say that something is remarkably or surprisingly effective, but that’s not what the original essay was about at all!

“The unreasonable effectiveness of the mathematics in the natural sciences” was a philosophy of science piece whose thesis was that there is no reasonable (rational, provable) basis for the degree to which our math abstractions and syllogisms happen to correspond to the physical universe.

It is self evident that they do in fact correspond super well, but the original piece was about how weird and spooky that actually is, if you think about it at all. Math is super effective, and there is no reasonable basis that we yet know that it should be so effective. It’s unreasonably effective.

It’s such a perfect title for that piece, and it feels dirty or diluting when it’s just used to mean “remarkably effective.”

[+] mekoka|4 years ago|reply
IDE vs Text editor. OOP vs Functional. Logger vs debugger. The holy wars that shouldn't be. Why can't we all be friends and accept that Vim is better than emacs.
[+] fao_|4 years ago|reply
I used to think like you, friend! For over a decade, then I discovered doom emacs :)
[+] Tade0|4 years ago|reply
Let us not forget the war to end all wars:

Tabs vs spaces

[+] lamontcg|4 years ago|reply
I've been a vim print debuggerer for like 30 years, and last year picked up writing C# in an IDE (Rider) and its been quite nice really.
[+] Androider|4 years ago|reply
Speed of iteration beats quality of iteration.

You can step through the program, reason about what's going on, tracking values as they change. But if you missed the moment, you have start again from the beginning (time traveling debuggers being rare). Or maybe you're looking at the wrong part entirely at this stage, and just wasting time.

With print debugging you write a bit of code to test a hypothesis. Then you run it, and you keep running it, and especially if it's an UI program you play with the UI and see how the values change during that run. Ideally the loop to change the code -> see the result should be a few seconds.

You can then git commit or stash your prints, switch branches and compare behavior with the same changes applied. And at the end of the day if you walk away, your prints will still be there the next morning. The debugger doesn't produce any comparable tangible artifacts.

Once you do know where the problem is, and if it's not apparent what the problem is (most problems are pretty trivial once located), that's IMO the time to break out the debugger and slowly step through it. But the vast majority of problems are faster to solve through rapid iterative exploration with prints in my experience (C, C++ for over a decade, Python, now JS/TS).

[+] goalieca|4 years ago|reply
Print debugging is the only way in a distributed system the way we are building micro services these days. We just call it logging.

Edit: ..and do it in production

[+] kstrauser|4 years ago|reply
Amen. “What do you use for debugging prod services?” “CloudWatch.”
[+] yaantc|4 years ago|reply
Yes. The same apply to a lot of embedded systems. When you can't stop the system you better learn how to debug from logs. And put the right logging support in place ahead of time: it may be impossible to replace the software in place by a new debug version, and then it's only based on preexisting logs and just adapting the logs configuration.
[+] roca|4 years ago|reply
It doesn't have to be that way, record-and-replay debugging can overcome this.
[+] wodny|4 years ago|reply
Print debugging is not that different from setting logging level to DEBUG and those logging calls should already be there in code and give meaningful insight so I don't get printing being often ridiculed.

For over ten years of commercial work I used a debugger only a couple of times and in most cases it was against someone else's code, usually when things were completely broken and I needed to get backtraces from multiple deadlocked threads or lacked debugging symbols and things like radare were also required. There were also times when I manually called a syscall using gdb.

My opinion is that if you can't reason about the code helping yourself with just a couple of additional messages the code is probably broken/too complicated to begin with and requires serious refactoring. I've never understood people stepping through a program hoping to find some mysterious creature somewhere along a huge stack of calls. In my career I have often seen people always debugging an application as a whole instead of separated modules. Dividing a problem is the key. The same key that allows me to still program using vim without autocompletion, keep APIs sane and coherent, and avoid dead code.

One really useful exception is when dealing with electronics. My friends programming hardware use debuggers all the time and in this case it actually makes perfect sense because there is no way to print anything and things like hardware interrupts come into play.

[+] saagarjha|4 years ago|reply
> My opinion is that if you can't reason about the code helping yourself with just a couple of additional messages the code is probably broken/too complicated to begin with and requires serious refactoring. I've never understood people stepping through a program hoping to find some mysterious creature somewhere along a huge stack of calls. In my career I have often seen people always debugging an application as a whole instead of separated modules. Dividing a problem is the key. The same key that allows me to still program using vim without autocompletion, keep APIs sane and coherent, and avoid dead code.

The big thing here is that you seem to only work with your own code, where you can arbitrary refactor it and keep the entire thing in your head, as well as quickly find which module does what. But when working with a large foreign project, none of this works. You have to start working at the scope of the entire program, because you have no idea of the internal structure yet. Of course, people who use debuggers divide the code up as they go, but the point here is that they place a few choice breakpoints at central points in the application logic, inspect the stacktraces when one gets hit, and use them to further dig in to the part of the code they need to look at.

[+] blauditore|4 years ago|reply
> I used a debugger only a couple of times and in most cases it was against someone else's code

The vast majority of code I investigate is "someone else's" code. Most of the cases, it's a historical accumulation by multiple authors. If you generally only work in your own code, that's quite a different experience, and debugging is generally easier (because you were there when it was written).

[+] bsder|4 years ago|reply
Actually, using the UART interface to send text breadcrumbs out the port is a standard technique in embedded, too ...

The article hits the point of print debugging, you get to see the backward in time.

By the time you hit "the problem", the pointer is NULL, the memory is trashed, the system is deadlocked, etc. You need to reason about how you got there.

There is a reason why the next step up from basic debugging embedded is "streaming trace"--effectively print on steroids.

[+] xzel|4 years ago|reply
I pretty much all of these. One thing I wanted to add is decorators. There is code you might have easy access to edit to add print statements. I don’t love the spring boot docs and reading the code isn’t as useful as stepping through your specific autowired code tree. There’s definitely use cases but 95% of the time prints will get you there. Imo you should learn it because it will save you a bunch of time and headache when you need it.
[+] lanstin|4 years ago|reply
When I start to use a new server framework, I like to step thru the main loop, just to see how it works with system calls/listens/accepts/reads and how it dispatches up the stack. But for debugging, I like to a) make it reproducible, b) read the code, c) add logging to help with any deductions that b yields. (Sometimes will just go to b if it's a simple bug).
[+] steelframe|4 years ago|reply
> I don't get printing being often ridiculed

I just told one of my co-workers last week that I was going to print-debug an issue. He paused for a moment before saying, "Uh, I can just debug this for you if you like."

So yeah, there's definitely some kind of stigma against print-debugging.

[+] humbleMouse|4 years ago|reply
You’ve only used a debugger a couple of times in 10 years? Yikes.
[+] adam_arthur|4 years ago|reply
I've never understood print debugging, at least in a web dev/nodejs context.

I don't begrudge people having their own approach to things, but almost universally when I see people use print debugging they seem to take quite a bit longer than just break pointing at the problem area.

If your code is in an unexpected state, it's much easier to hit a breakpoint, examine local values, and then backstep through the call stack to see what went wrong. I dare to say that in a single threaded context, it's almost objectively more effective.

Versus the alternative of using printlines, you basically need to map/model the state flow out in your head which is prone to error (limited capacity of human working memory).

Is it not easier to directly see the problem rather than doing mental math to make assumptions about the problem? I can't see a case for that being more effective.

Most of the time I see people print debugging it seems to be because they haven't used the debugger much... either they aren't comfortable with it, or didn't bother to set it up, or see the mental mapping approach as more "mathematical/logical"... or something. Takes you back to the school days of solving algorithms on paper :)

That being said for simple problems, I've used print debugging myself (again, usually because I'm too lazy to setup the full debugger). Or for multithreaded contexts etc, where thinking it through can actually be more effective than looking directly at the problem (multiple contexts)

[+] roenxi|4 years ago|reply
Probably the most interesting thing about development as a discipline is the near radio silence on how to debug.

There is a decided lack of academic success in engaging with debugging as an object that can be studied. There are channels to learn about debugging as a stand-alone topic. Programmers don't often talk about debugging techniques in my experience.

For something that takes up the overwhelming bulk of a developer's time the silence is in many ways deafening. It may be that nobody has a method superior to print debugging.

[+] razorfen|4 years ago|reply
ITT: a non-controversial opinion shared by most programmers.

Print debugging is fast in many cases and requires little mental overhead to get going.

But for some/many systems, there's a huge startup and cooldown time for their applications - and compiling in a print, deploying the service, and then running through the steps necessary to recreate a bug is a non-trivial exercise. Think remote debugging of a deployed system with a bug that requires select network and data states that are hard or impossible to replicate in local/dev.

For things like this, being able to isolate the exact point of breakage by stepping through deployed code, and doing immediate evaluation at various points to interrogate state can't be beat.

This post strikes me as either (a) a younger programmer who still thinks that tool choice is a war rather than different tools for different jobs (b) someone making a limp effort at stoking controversy for attention.

[+] DangitBobby|4 years ago|reply
> I should emphatically mention: I’m not saying that print debugging is the best end state for debugging tools, far from it. I’m just saying that we should reflect deeply on why print debugging is so popular, beyond mere convenience, and incorporate those lessons into new tools.

I'm not sure what about the article makes you think either a or b. They are trying to critically examine why some people reach for print debugging first, and I think it's spot on.

[+] tyingq|4 years ago|reply
Probably explains why java has such a rich set of logging and debugging tools. Startup time, plus the idea that printing to stderr/stdout doesn't help you figure out where that goes in many java environments :)
[+] jollybean|4 years ago|reply
Or c) someone just making comments from observed experience, and there's not much about that 'senior developers' have when it comes to 'having had to compile something that takes a while' - that's the purview of everyone, or at least, those who have worked on those larger projects. And though remotely debugging code definitely happens, it's in relative terms, very rare. This is just someone making a comment on their blog, that's it.
[+] CJefferson|4 years ago|reply
On the other hand, when you are working in an example like you are discussing (a service, or multiple services, which must all be deployed), it can be hard to figure out how to get the debugger attached.

It possible depends on the kind of programming you do -- I find myself doing little bits of work on projects in many languages, so learning how to get the debugger going often takes longer than finding + fixing the bug.

[+] diminish|4 years ago|reply
In languages where you build a deeply nested call stack, advanced debugging looks more promissing. But in simpler setups like ASP/PHP/JSP etc, simply printing works fine.
[+] roca|4 years ago|reply
Almost all the reasons people use print debugging can be overcome by improving debuggers --- and to some extent already have been (in the words of William Gibson, the future is already here, it's just not evenly distributed yet). I think it's important for people to understand that the superiority of print debugging is contingent and, for many developers, will not persist.

Record-and-replay debuggers like rr [0] (disclaimer: I started and help maintain it), Undo, TTD, replay.io, etc address one set of problems. You don't have to stop the program; you can examine history without rerunning the program.

Pernosco [1] (disclaimer: also my baby) goes much further. Complaints about step debuggers (even record-and-replay debuggers) only showing you one point in time are absolutely right, so Pernosco implements omniscient debugging: we precompute all program states and implement some novel visualizations of how program state changes over time. One of our primary goals (mostly achieved, I think) is that developers should never feel the need to "step" to build up a mental picture of state evolution. One way we do this is by supporting a form of "interactive print debugging" [2].

Once you buy into omniscient debugging a world of riches opens to you. For example omniscient debuggers like Pernosco let you track dataflow backwards in time [3], a debugging superpower print debugging can't touch.

rr, Pernosco and similar tools can't be used by everyone yet. A lot of engineering work is required to support more languages and operating systems, lower overhead, etc. But it's important to keep in mind that the level of investment in these tools to date has been incredibly low, basically just a handful of startups and destitute open source projects. If the software industry took debugging seriously --- instead of just grumbling about the tools and reverting to print debugging (or, at best, building a polished implementation of the features debuggers have had since the 1980s) --- and invested accordingly we could make enormous strides.

[0] https://rr-project.org

[1] https://pernos.co/about/overview

[2] https://pernos.co/about/expressions

[3] https://pernos.co/about/dataflow

[+] georgewsinger|4 years ago|reply
I recently discovered a Linux debugger & tool which allowed me to solve problems 10x faster than print statements: pernos.co (which is layered over Mozilla's rr time-tracking debugger).

Pernosco's tool is described pretty well on their website, but basically it allows you to view a program inside and out, forwards /and/ backwards, with zero replay lag. Everything from stack traces to variable displays (at any point in time in your code execution) is extremely easy to view and understand. The best part is the lightning fast search functionality (again: zero lag).

On top of this: extraordinary customer service if anything breaks (in my experience, they fix bugs within 24 hours and are highly communicative).

If you value your time I highly recommend you check out this tool.

[+] jhgb|4 years ago|reply
I had a crazy idea the other day that perhaps there could be something like "CSS for program execution traces". If you think of function identifiers as XML/HTML tags and arguments for individual function activations as element attributes, then perhaps something similar to CSS selectors but acting on the tree representation of a program's execution could trigger at certain clearly defined points during the execution and format some human-readable output of what the program was actually doing, or a "cross-section" of it at least.
[+] zmmmmm|4 years ago|reply
Most under appreciated aspect of proper debuggers is not about the code line of interest but the context they give you about the whole application, ie: the stack frames and their state. When handed a new codebase I often fire up the debugger and attach and set various breakpoints in interesting places and then execute the application to see where / when they get hit. It's a great way to learn a codebase - things that are hard to discover ("when is the database driver created and how does it know its password") just pop out where you might have to spend ages working it out if you were just examining the source tree.
[+] corysama|4 years ago|reply
In my experience, people who downplay debuggers don’t have the option to use effective debuggers. Debugging C++ and especially C# in Visual Studio is wonderful. Debugging Java in Eclipse can be great. Meanwhile GDB and most other language debuggers are painful and every IDE integration I’ve seen of them has been horribly unreliable.

I’ve heard there’s a culture in parts of Google where kids go through uni using GDB because “Woo Linux!” then go straight into Google where everyone is “Woo Linux!” (I do like Linux, btw) so they are either still using GDB, or more likely have given up on it and reverted to printf. So, everything takes forever to figure out and that’s just “normal”. This was coming from a console gamedev who was shocked by the transition after moving to Google.

Meanwhile, I’ve spent a good part of the past couple decades debugging large volumes of code that I will literally only see once ever. With a good debugger, that can be done effectively because watching and even modifying the code’s behavior can be done at a glance rather than a re-compile.

I’ve also worked on a very big project that used extensive logging because they had a very bad debugger setup and productivity was in the toilet compared to every other job I’ve had. The only way I could keep productive was to take the time to break out systems into small independent programs in my own environment so that I could use a debugger on that rather the run the code where it is.

[+] SPBS|4 years ago|reply
I feel like the author gets close to the point but fails to drive it home: step-through debugging is unbelievably cumbersome. During a typical step-through debugging session, 90% of the time is spent on lines you are completely not interested in. Oh, did you accidentally skip the important point because of how tedious it was to keep spamming step-over/step-in? Better start over again. With print debugging, you set up your print statements strategically and -zing-, you get your results back. Feedback loop shorter. 100% of the lines are the ones you are interested in, because you put the print statements there.

I'm still waiting for the feature where you can conditionally stop at some breakpoint -only- if some other breakpoint/watchpoint was crossed over. It's not a conditional breakpoint, because conditional breakpoints can only watch variables, not other breakpoints. You could of course set some variable depending on whether some section was entered and then conditionally break based on that variable. But then you're back to print debugging land, having to manually insert code in order debug the program.

Debuggers are superior when it comes to interrogating the exact state of some variables, as well as the decision paths the program takes. For anything simpler, print debugging simply offers the better developer experience.

[+] Stratoscope|4 years ago|reply
> I'm still waiting for the feature where you can conditionally stop at some breakpoint -only- if some other breakpoint/watchpoint was crossed over.

PyCharm 2021.1 has this, so I would guess that other members of the IntelliJ family probably have it too.

Set a breakpoint and then right-click the red dot, and click More to open the full Breakpoints dialog. Open the drop-down under "Disable until hitting the following breakpoint:" and select the other breakpoint that should enable this one.

And thank you for mentioning this! I didn't know PyCharm had this feature until I took a look after seeing your comment. This will be super useful.

[+] bc4m|4 years ago|reply
Why would I spend any time stepping through lines I'm not interested in? I set breakpoints on the important parts and let the program run until a breakpoint is hit.

I stop at a breakpoint only after another breakpoint is hit all the time. You set the first breakpoint and run the program. It gets hit and pauses, you set the second breakpoint, then resume.

I'm just not getting how print debugging is the better experience.

[+] astura|4 years ago|reply
>During a typical step-through debugging session, 90% of the time is spent on lines you are completely not interested in. Oh, did you accidentally skip the important point because of how tedious it was to keep spamming step-over/step-in? Better start over again.

No offense, but this sounds like you just really need to learn how to use a debugger - This is in no way a "typical step-through debugging session." I've been a professional software developer for 16 years and I've never once in my life "spamm[ed] step-over/step-in"

>I'm still waiting for the feature where you can conditionally stop at some breakpoint -only- if some other breakpoint/watchpoint was crossed over.

This is trivial to do, place two breakpoints, disable one. When the breakpoint is hit, enable the second (and optionally, disable the first).

[+] kmfpl|4 years ago|reply
With lldb you can do that, basically you have the option of running commands when a given breakpoint is hit, so you can just make it place another breakpoint, and it will be placed only if the first breakpoint is hit. I assume you can do something like this on gdb as well.
[+] arendtio|4 years ago|reply
Another aspect, where printf debugging can be better than debuggers are use-cases where timing is relevant. Some bugs don't occur when break points stop the program at certain points in time. For completeness is should be added, that there are also cases where the printf can change the performance and make it impossible to find a bug.

I think the two methods are complementary and should be use in combination.

However, the big issue is that basic printf debugging is very simple to use and debuggers have a steeper learning curve in the beginning. Therefore, people start using printf debugging and don't invest into learning how to use debuggers. And when developers don't invest into learning how to use debuggers properly, they are missing the skills to utilize them and still use printf debugging in cases when debuggers are clearly superior.

[+] mjw1007|4 years ago|reply
There are two separate questions: whether you want to see some kind of trace of the program or you want to step around in its state, and whether to use a "real" debugger or not.

In most cases I prefer to do something trace-based, and in the IDEs I've used the debuggers have much weaker support for that than they do for stepping around.

In particular, setting up tracepoints tends to involve fiddly dialog boxes which are much less convenient than using the main text-editor interface to say what you want.

I think there's plenty of scope for debuggers to provide a better interface for trace-style debugging. For example I'd like to be able to toggle a tracepoint after capturing the run, and have the lines it created appear or disappear, or add a filter expression or additional information to display without having to rerun the program.

[+] jollybean|4 years ago|reply
Qt Creator debugger fails on me constantly, it's 2021 and the leading C++ plaf. is completely unreliable in that many more cases.

That's why 'I must' use print debugging, because the 'powers that be' still provide a broken, half-baked solution 30 years in.

Print debugging is however so powerful, I think there almost should be a mechanism built into languages and tooling around it so that it becomes part of the process instead of a 'kind of workaround'. It's something we all do, constantly, and yet you'll never hear about it when people are arguing about Rust or Go.

[+] njharman|4 years ago|reply
I think it has most to do with way user thinks.

I need to see big picture, whole state, all the stuff and rapidly jump back and forth. I also, supposidely, have ability to keep a lot of state / scope/ abstraction in my head. So I find print debugging sufficient and fast. Rarely encounter situation I feel need for "stronger" tool.

Where other people focus on one thing, all that simultaneous output is just noise and distraction to them. And based on the continued use and popularity of step-based debuggers, these people are much more productive (and happier) using those type of tools.

It's very important to understand neither system is inherently superior. Although one or the other is superior to each individual. [btw over 35yrs of tech industry / software development I've found this true, that tools/paradigms are not universally superior but are superior based on individual) for many subjects. All the ones that have internal debates in techdom]