365 comments

[ 0.26 ms ] story [ 308 ms ] thread
I usually only do print debugging when I encounter a Heisenbug. I mainly develop in Java, maybe my choice is related to its really really great debugging tooling.
The worst is when you put print statements in and a bug goes away, and you realize there's some kind of instruction reordering bullshit at work.
A simple toString() with side effects can be insidious when mixed with other bugs...
I have been developing large-scale django apps on ec2 for a while and the solution that has been working best for me is a lot of logger.** statements sent to papertrail.
Don’t let debugger heavy users tell you off
Why "unreasonable"? There's nothing unreasonable nor wrong about print debugging. Moreover, it's a great first step towards logging and testing.
It’s “unreasonably effective”, meaning that a cost/benefit analysis is very clearly tilted towards benefit.
Besides "behaviour in time", print debugging is effective because it's typically an extract of the most interesting for programmer things. I have a debugger window open this very moment and I can see about a hundred lines with various information about one structure, but I'm interested only in two (and I have to rerun this several times, because state got corrupted somewhere earlier).
I stopped doing step debugging at all many years ago. For me it looks the same as visual vs. text programming. Text and text search tools are just miles ahead of clicking buttons.
There's always gdb/lldb from the command line.
So (for instance) in PowerShell my code breaks right as it is about to fail, with the state intact (ErrorActionPreference Break) which allows me to effectively fix whatever problem is about to occur and immediately have the state at the time of failure.

I dont understand how printing text could EVER approach this given I can test my assumptions right away and generally only need 1 error to happen to understand the totality of the circumstances.

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.

This kind of thing actually got added to a Microcontroller and its native SPIN language.

There is "SEND" which can be used as an ad hoc comms channel, aimed at a program method.

And a debug system that can both stream data, text and graphics to a client, as well as capture and report on the state of the 8 CPU cores possibly running.

https://www.parallax.com/propeller-2-graphical-debug-tools-i...

The debug output is something like using an xterm with Tektronix emulation turned on, and with all the lower level bits packaged away. The use can do a lot, from a "print" type operation to sophisticated graphics, static or animated.

On the capture side, a sort of supervisor region of RAM is reserved to for an ISR to capture processor state, or anything in memory really. Can be time, or event driven.

Have you tried Clion?
Well I just tried it and thanks for reminding me it was an option. I like it so far, we'll see how the debugger works, but IDE's cannot rid us of the cobwebs of arcane languages. C/C++ I think are the worst, the number of pitfalls and amount of needless complexity is byzantine and an enormous strain on mental energy.
So I'd be curious. I usually work in scripted languages Bash, Ruby, JS (bleh) a bit of python.

Sometimes I do some Java work though and I usually end up going to print debugging because trying to figure out all the Java logging framework, or not ending up like 40 layers deep in some magic framework dependency that is interecepting my code which is what always happens when I use a debugger.

That being said do those who work in compiled languages make more heavy use of debuggers?

> do those who work in compiled languages make more heavy use of debuggers?

I work in both quite a bit. I actually think I end up using a debugger more in e.g. Python because I'm more often asking questions like "what is the type of the thing being passed here", which is not a thing I need to seek out in something like Go.

That said, I think it's more a style difference than anything. I use debuggers in both compiled and noncompiled languages when I need a deeper look, and I'd guess people who don't use debuggers in scripting languages wouldn't use them in compiled languages. Probably also has to do with the ecosystem and how easy/effective debuggers are.

I wonder if C# is a bit of an outlier here. I work mainly in C# but also sometimes do Type/JavaScript. While I've got the VS Code debugger running for JavaScript projects I'll rarely use it and generally use print debugging.

In C# I'd almost never use print debugging and the whole thing seems ridiculously antiquated (it's partly why I hate JS work). [Assuming you're working in Visual Studio...] You literally hit 1 key, F5 and then you can step through, time travel, edit and continue. I wonder if people just haven't experienced the ease of debugging in .NET with VS. I'd say I write probably 50% of my code in a debugging session, edit and continue is a game changer.

I did a little Java work in Intellij and it was similar but I think partly due to a lack of familiarity with the UI didn't feel quite as powerful.

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

Amen. “What do you use for debugging prod services?” “CloudWatch.”
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.
It doesn't have to be that way, record-and-replay debugging can overcome this.
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.

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 :)
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.
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.
> 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.

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.

For python: i specifically recommend https://github.com/zestyping/q a lot, which is like print debugging on steroids:

  All output goes to /tmp/q (or on Windows, to $HOME/tmp/q). You can watch the output with   this shell command while your program is running:

  tail -f /tmp/q
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.
I heard the latter sentiment earlier today, but I don’t think anyone is actually passionate about what editor others use. Opinionated sometimes.
Most of the time, I suppose. Watching someone try to write java in vim (or generally, without an IDE) gives me anxiety though, even with a language server :)
Meh, it's fine. In general, I find that vim in more productive most of the time (now I have a language server, before, wouldn't ever consider it!)

The fluid and consistent (java is only a portion of what I write at work) editing experience is mostly more valuable to me than the slightly better autocompletion.

I keep intellij installed, but it only open it if I want to do a fancy mechanical refactor, like extract an interface from an existing class. Smaller niceties like creating a local variable from an expression are only a handful of keystrokes just feel like naturally describing what I want (lexically, rather than semantically, I'll admit) in vim anyway.

That's perfectly doable. I routinely navigate / extend / debug / refactor a 600 KLOC Java codebase with nvim + ctags + ripgrep and will have the job done well before the language server has even completed digging through those 600 KLOC.
Passion about the tools others use can be called for if you can see they're obviously struggling to meet their goals with the tools they've chosen.

The hard part is, unlike a screwdriver where you can demonstrate, editors and "IT" in general are mental tools where the mindset is an invisible, nontransferable "handle" to the visible portion that everyone can see and use.

A previous manager was snarky about me using Emacs to write Python instead of “a proper tool”. Every time he’d pass my desk, “a real IDE could to that for you”. We finally had a conversation along the lines of “can it save me more time than you waste pestering me about meaningless stuff? Also, STFU until I miss a deadline for the first time since I’ve been here.”

I would not hire a carpenter who doesn’t believe in using hammers. Neither would I constantly bug a hired carpenter to use the hammer I think they should be using instead of the one they like.

I used to think like you, friend! For over a decade, then I discovered doom emacs :)
For those wondering, Doom Emacs is a better vim (from evil-mode) than vim and so much more (easy out of the box community configs for most languages and tools, and way more cool stuff) inside the Emacs OS.
I tried using it but got stuck on having to learn Lisp to understand my config file.
Apparently all the cool kids are using neovim + Lua these days. Lisp turned me off of emacs years ago as well. Recently I started digging into Neovim and have found Lua much easier to parse/internalize than Lisp, and kind of a joy to work with.
Great, I am at least halfway on the right track since I am using neovim. I know Lua a bit, I actually didn’t realize it was integrated.
I'm not sure what's difficult about

    (map! :n "ff" #'save-buffer)          ; Save
    (map! :n "fq" #'kill-current-buffer)  ; Quit a buffer
or

    (defun my/org-buffer-check ()
      "Check that we are in org-directory, and the buffer name is in that directory"
      (and (string= org-directory default-directory)
           (seq-contains (directory-files org-directory nil)
                                (buffer-name)
                                'string=)))
the latter being easily represented as:

    function my/org-buffer-check()
        return string=(org-directory, default-directory)
           and seq-contains(directory-files(org-directory, nil),
                            buffer-name(),
                            &string=)
    end
(seq-contains looks through the sequence returned by directory-files, for the result of buffer-name(), and compares them using the string equality function)
Sorry but that is not clear or obvious at all to me.
I understand the "!", I use it when it fails the first time and I'm trying to hint that I really really want it to work.
What always prevented me to actually switch to Emacs was how it's so huge it seems impossible to get an overview of how to do what. Every programming language-specific mode comes with its own unique features that surprise me when I just want to write code, meanwhile just entering a single tab without it getting deleted again is an odysee of reading documentation. At the same time it's slow and despite it having the best Vim emulation it cannot hide that Emacs just doesn't work like that. As soon as you leave the file's buffer you discover how Evil mode's illusion falls apart on all sides and you always land in situations where you have to use a mix of Vim and Emacs keybindings.

I love the concept behind Emacs, I just think at least 80% of its code should actually be in plugins, and the program itself and a lot of large expansions are really bogged down by the sheer size and lack of simplicity.

Oh, and Emacs-Lisp...it's much better than Vimscript, but it's a disappointment nonetheless. Loops instead of recursion in Lisp, really? And last time I tried it the parser could not handle unmatched brackets in comments.

> Loops instead of recursion in Lisp, really?

That's pretty common in common lisp as well. Specifically do loops (and lest we not forget the loop macro).

I think you might be thinking of the scheme branch of lisps, but not all of them work that way.

Wow, by looking at this screenshot (https://raw.githubusercontent.com/hlissner/doom-emacs/screen...), is doom emacs a terminal/console program or a GUI program?!
It's a number of extremely, extremely well crafted layers on top of Emacs.

I switched last July and after 8+ years of using variations of Vim, Vi, Ex-Vi, Ed(1) (yes), NeoVim, et all, it is by far the smoothest experience I've ever had.

Unlike my experience with Spacemacs, I haven't had any problems adapting from Vim -- there are no points where the Vim interaction layer breaks down, and it genuinely feels like an editor that I'll be using for the next 20+ years. Like something that can grow around me.

It's configuration boilerplate for emacs. Emacs itself can run in both terminal and GUI mode, and doom should broadly look similar in both settings.
Emacs (Doom Emacs included) can run in a terminal session or in a GUI.
Spacemacs is the one true way, heretic scum!
(comment deleted)
How's file exploring and method/class definition lookup these days in Vim?
The quality of the language servers vary, but you could get a decent IDE-like experience.
LSP is a great leveler, for that. From what I hear, vim has great LSP support, these days.
For the junk I write and work with Ag/Rg has been sufficient.
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.
Let us not forget the war to end all wars:

Tabs vs spaces

Tabs are the clear winner here, since they unambiguously denote which level of indent is in use, take only one character (octet) per indent level, and can be visually adjusted on any moderately advanced editing program to an end users taste WITHOUT modifying the source code.

This __wouldn't matter__ if we all just used TABS for indent level and if spaces were ignored for that: I also prefer tabs to show in a GUI code editor at ~4 characters, but be equivalent to 8 display characters in terminal modes (I guess EM size, but when I care about those I really want a 'fixed width' font, so toss all of the complexity aside please).

> unambiguously

Multi people teams (or just one person editing on different platforms depending on need) using different editors and with different preferences would like to have a word with you here.

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.

I’m sorry but this is just ignorance, learning to use the debugger for the platform at hand is a basic skill every developer should master. So many times have seen developers use the debugger to troubleshoot and fixed issues and been perceived as a “ninja” (despise that term but that was the effect) because they knew how to use the debugger. I mean yeah keep printing lines, and keep being out performed by your debugger using peers. That’s the choice.

Yes I am dieing on this hill.

OK. And I've also cringed watching ninjas step through code slowly, reading everything, spending 20 mins catching something 2 print statements would have achieved.

Debuggers aren't bad. But neither is printing. Knowing when to reach for them is probably a bit more key.

They weren't ninjas, otherwise they would have used breakpoint actions for doing those print statements without modifying the source code.
What's breakpoint action? Is it like inserting printf before breakpoint?
maybe this is sometimes called assert()? or in some debuggers you set a watch on a var and the BP triggers only on the watch-condition so the BP don't trigger each loop, only when x=7
Breaking is just the default behavior when a breakpoint is hit, you can generally attach whatever behavior / conditions you want using the debugger's scripting language.
Reading through the majority of this comment section, I get the impression that those who like print statements find value because they aren’t proficient with modern debuggers, rather than they find print statement valuable even though they’re proficient with debuggers.
I once saw an interview with Visual Studio members that one reason why they started doing talks about how to use the debugger was the continuous set of requests for features that Visual Studio does almost since it exists.

Same applies to other debuggers.

It is not only the debuggers, but also OS and language runtime tracing facilities like DTrace, eBPF, ETW, JFR, ....

Many devs aren't 10x because of QI, rather because they learn and make use of the tools available to increase knowledge about the platform.

I agree, but my feeling is, if one person is bad at using debuggers it is their fault. If (as it seems to me) most developers are bad at using debuggers, then it's probably to debugger's (and associated tooling's) fault.
For me it is the teacher's fault, given that a large majority never teaches anything related to debuggers.

So we get generations that use vim and Emacs like notepad, create Makefiles by copy-paste and barely know gdb beyond set breakpoint, run, step and continue.

Using C as example, but feel free to extrapolate to another language.

And no I am not exaggerating, this was the kind of students I would get into my lab when I spent a year as TA in 1999/2000, and naturally would have to get them up to speed into good programming practices.

It maps breakpoints to debugger actions that are triggered instead of actually stopping execution, like formatted output of whatever variables are in scope.
Tools in a toolbox.

The worst developers I've ever known always have their "this is the best way to do everything" hill they die on.

> And I've also cringed watching ninjas step through code slowly, reading everything, spending 20 mins catching something 2 print statements would have achieved.

The problem is that the two print statements will only catch the bug if they are the right two, based on a correct hypothesis of what the bug is. Which, with a debugger, won’t require stepping, but setting two breakpoints, doing a run-to-breakpoint, and inspecting values.

Stepping is required when you are exploring behavior because you don’t have an easily testable hypothesis about the source of the bug.

But with print statements if the first place I put them doesn't work, I can start doing bisects and quickly find the right place to print.

As you note, debugger breakpoints aren't magically better than print statements when I'm investigating a hypothesis – I'm no more likely to put them the right place than I would have put print statements.

And then there's a class of problems that neither debugger nor print statements will help: many years ago a very junior co-worker was wondering why his C code was giving the wrong answer for some math. It took me pointing out that one of the numeric types he was using in his code was different from the rest (I think it was #defined elsewhere, in some library, as an integer type). When the compiler did the math it had to do some type coercing.

> And then there's a class of problems that neither debugger nor print statements will help: many years ago a very junior co-worker was wondering why his C code was giving the wrong answer for some math. It took me pointing out that one of the numeric types he was using in his code was different from the rest

A debugger and watches on the values of concern absolutely will help with that (so will properly placed print statements), so its a really bad example. (Of course, strong typing helps even more with that particular case.)

No, my co-worker was so junior he didn't understand why that was happening. It took me a moment to glance at the types in the source and point out the problem, no debugger needed.
> But with print statements if the first place I put them doesn't work, I can start doing bisects and quickly find the right place to print.

Can't I also bisect with breakpoints?

You can, but the comment was talking specifically about stepping through the code line-by-line.
I can bisect faster with breakpoints. Plus with a time traveling debugger like rr the the time is further reduced.
I've used print debugging extensively doing embedded development, when I could reasonably hook up a serial port to capture the output, or put a crude console on a tiny screen. These systems can't always be debugged in the traditional sense, and if you're troubleshooting some bug that happens on hardware but not in your simulator, then you use the tools you have available.
(comment deleted)
I do use debuggers when I have a hard core problem, I've found some horrible memory corruptions using RR and gdb for reverse debugging. However, sometimes someone throws a horrible gradle java project at you and asks for help, and figuring out how to debug is a pain.

I'd throw your comment back -- maybe all languages should be "debug first", make it as easy to get code into a debugger as it is to just build and run it.

You are missing out on the important point: printing forces you to formulate a hypothesis: what you expect and to compare with what you actually get. Debugging encourages less modeling and more trying random stuff until something sticks.

It is an exaggeration. In practice, it is useful to apply both. Novices can get some insight using debugging, more experienced with code base people should exercise their understanding of the code and use well picked prints.

You can also do print debugging with a debugger. Just have a breakpoint that doesn't halt, but instead simply prints the values of interest to the debugger console. This is particularly nice for things like debugging interrupt handlers where the time taken to print output normally is too much to accept.
Watched variables they were called once upon a time.

Also, I think what you suggest to do here is way harder to learn than printing.

Perhaps, but that lends credence to the theory that people using print debugging may just not have learned how to effectively use a debugger yet.
I’m also willing to bet you live way up the stack.

Try debugging something in the embedded world, and you’ll see why a lot of bare metal programmers use printfs. Turns out timing is critical most of the time, so using a debugger hides a LOT of bugs from your eyes.

Debuggers are very useful, but so are prints, there just different tools, they have different purposes.

(comment deleted)
I agree with this sentiment. Print debugging is essentially universal. The same goes for compiler directives or simple constant driven conditionals to toggle it (in a brute force way) on and off.
What Python debuggers you are talking about? have you tried the built-in CLI debugger? Just drop breakpoint() in your code and you're in. Have been using it daily for over a decade and really happy with it - it's actually one of my favorite features of the language, amongst the many super useful features that Python and its excellent stdlib have to offer.
Honestly, I didn't know that and I'll try it. Last time I had to debug I remember adding "-m pdb" (as at the start of https://docs.python.org/3/library/pdb.html , first result in Google), but for some reason that immediately threw an error instead of starting the program, so I just chucked some prints in instead.
I have mapped a key to insert "import pdb;pdb.set_trace()" in my editor. Also use it daily, and not just for debugging. It is useful when working on a new project and you just want to interrogate some object you got back from a library to see what valid operations you can do with it. Or to double check some math operations.
I just tried python -m pdb and it works for me https://dpaste.org/9EQo#L26 but I really always use breakpoint(). You can even configure it to use other debuggers with an environment variable, ie.: PYTHONBREAKPOINT=ipdb.set_trace
I just quickly went any checked the program I was trying to debug. I was running 'python3 package --arguments', where 'package' is the name of a directory which contains a package I was working on. 'python -m pdb package --arguments' just complains that 'package' is a directory.

Adding a 'breakpoint()' at the start of the program does get me into the debugger. I'll remember that for future (but, it's not easy to find by googling if you don't already know what you are looking for!)

For complex problems, `import pdb; pdb.set_trace()` instead of a print statement can be super handy. It basically launches the debugger from the context of wherever you stuck the line.

For large unwieldy data-structures, you can go ipython: `import IPython; IPython.embed()` launches the ipython REPL from the calling line's context.

I use the latter a fair bit when spelunking around in other people's code. `pdb.set_trace()` lets you continue execution more easily.

Never felt that Python debugging was "fragile". BTW if you're not using pudb you're missing out.
Only thing I hate about this .... regular point of code review: remove the debugger breakpoint you left in your code!

We haven't had one hit production yet, but it came close. Print statement is a lot more harmless.

(comment deleted)
As a counter-point, I think there’s an argument that folks don’t spend enough time in the debugger. But there’s a lot of value there and in fact one could use a debugger environment to unit test as even native debuggers have scripting environments.

Personally, I think folks should master the debugger _first_ and during all steps learning a programming language.

But similar to test-driven-development it’s a different way of thinking, and most books scarcely discuss the debuggers.

That being said, I do use print-debugging a lot too—in C++ a lot of functionality can be compiled-out, allowing one to, for instance, print hex dumps of serialized data going to the network.

On that note, there is a distinction between trace debugging that is part of the source code and general print statements that are hacked in and removed.

I can believe there is value in learning a debugger, but debuggers could stand to improve significantly. Debugger UX is almost universally awful and per the parent it’s often difficult to get one up and running. Moreover, if you do your “print debugging” with log statements of the appropriate level, they can be useful in production which is perhaps the biggest value.
Also, in almost all languages debuggers are an afterthought. Take e.g. the situation with Golang, Haskell or Python. Either there is no useful debugger or there is one, but it came late and still cannot debug everything the language does.
Print debugging not (really) working is haskell is... Non-idea, and a bad pairing for bad real debugging. But test cases are usually easier to figure out. Presumably there's a balance discovered by people in big projects but it never seemed as good as normal approaches to me.
Haskell debugging by testing is great for small functions where you can use quickcheck. But larger tests for the more complicated stuff don't work in quickcheck and there isn't much else that one can easily do.
I haven't actually used quickcheck in Haskell, but I've used it for very complicated tests in other languages including Racket, TypeScript, Rust, and Java. The nicest thing about quickcheck is that it lets you easily create test data without imposing too many constraints on it. Regular fuzzing or randomized testing is almost as good, but the narrowing done by quickcheck is sometimes nice for understanding test failures.
Not sure what you mean, there's e.g. Tasty for non-QC testing. It can do all sorts of variations of test, e.g. traditional unit tests, "golden" tests, etc.
On the other hand, it is reall great in case of the JVM
Not sure what situation you are talking about. Debugging Python is as easy as right-clicking a file in Pycharm and pressing debug. Why care if it was an afterthought when it for the past decade has worked perfectly.
I care. That it has worked for the last decade only means that Python was without a working debugger for 2/3 of its existence. 1/3 of which I had to suffer from it.

Also, pycharm isn't really what I would call a proper debugger yet, attaching remote running processes for example just doesn't work reliably yet and is very new anyways. Debugging embedded targets just doesn't work. Multithreading is iffy (but that's unfortunately normal in Python).

Also needing to use a particular text editor to use a decent debugger is bananas.
In a world with perfect optimizing compilers that never introduce bugs, we should never "need" print debugging. But that's not where I live, so I'll keep using print debugging.

On the other hand... adding print statements can also invalidate certain optimizations (an excellent source of heisenbugs), so I'll never stop using debuggers either

Print debugging is essential in distributed systems. Sitting in the debugger waiting for human input often leads to timeouts and not covering the case you want. Of course, sometimes adding the prints, or even just collecting values to be printed later also changes execution flow, but like do the best you can.
(comment deleted)
All the more reason for folks to spend more time using the debugger; for instance, break points are only one feature.

Setting up remote debugging I’ll agree is more difficult than a local application, but each remote machine can automatically run startup commands and not require user input; commands can be run at particular places too (to print output etc) with conditional trace points, all while not impacting the code itself.

Main point is that folks don’t spend enough time learning the debugger, as print statements are easier. But using the debugger is a better practice in my opinion in the case where print statements are added just for a quick test, then removed.

The problem is that it's not just a matter of "learning the debugger for Java." In practice there are many different projects that configure debugging many different ways, and it doesn't matter that you know which keys to press in IntelliJ if it will take you an hour to figure out how to attach it to the project. This speaks to OP's point, where it's hard to use a real debugger to casually investigate random projects.

Having said that, it is absolutely a requirement when working on a project for any length of time (especially professionally) to set up and figure out a debugging environment, because it is significantly more productive than printing. But the startup cost is certainly there.

> The problem is that it's not just a matter of "learning the debugger for Java."

In the Java case, for stanalone projects (i.e. not something deployed on a server) an if it is your own project and you don't do anything unreasonable it is mostly just set a breakpoint and hit "run with debugging".

Probably the least painful debugging experience I know.

Doing it for Tomcat/Tomee was slightly more advanced IMO but still utterly trivial compared to wrangling css or js ;-)

There are reasons why we "old folks" like Java so much despite its verboseness.

The java case is actually pretty universal ... you run the JVM with debugging enabled (fixed string of flags) and then tell your IDE to attach to the JVM on the port you gave it. You don't need compileable source, can be on a remote server, different OS etc - if you have just the source for the bit you want to debug you can set a breakpoint in it and it'll stop there.

Being able to debug third party code in remote / hostile environments (even when its mixed with proprietary vendor code) is one of the things I like about Java.

The Java case is arguably the least difficult out there thanks to reasons you outline. But still, the other day I had to debug a Gradle plugin written in Java. It's possible! But it took an hour or so of effort to figure out which options to use and which program to give them to.
Python easily has the best debugger I've ever seen in a language.

``` import pdb; pdb.set_trace() ```

that's literally all you have to do at any point in your code. Run your code in the foreground of a terminal, and boom you have a debugger exactly where you want it.

In recent Python, it's even easier -- just put `breakpoint()` in your code.
Fwiw, it's not too hard to approximate that in c/c++. Print out the pid (I often print out "gdb -p %d") and then sleep (and perhaps send SIGSTOP to other processes in more complicated scenarios).
Pry with Ruby allows you to drop in to a repl anywhere with binding.pry

You can edit the state and keep running after altering the program.

But usually I just end up printing a few things from the repl and figuring out what is fouled up.

>> If I have a Java Gradle project I have no idea how to get it into a debugger

You download Intellij IDEA, run it, choose File->Open and select the build.gradle file, right click the main class and there's a Debug option.

(comment deleted)
Part of the reason it works is it forces you to reason about the code, otherwise you won't know where to put the print statements.
Agreed. Also certain classes of bugs (such as bugs in parallelism) are easier to debug via print than using a debugger.
Print debugging always works, but also: it lets the programmer customize their view of the program’s (very, very large) hidden state in any way imaginable. Step debuggers are the “no-code” equivalent: extremely useful for the purposes for which they were designed—and often the better choice there—but inherently limited.

Geoff’s not wrong in invoking Bret Victor’s Learnable Programming argument that being able to track state over time is critical to debugging, and Geoff’s right that print debugging makes this easier than almost any existing step-debugger.

Bret’s deeper point, though, is that a major challenge in debugging is hidden state in general, and that variable state changing over time is just one example.

Not only is there a ton of hidden state in the execution of a program—the full execution trace PLUS state of every variable at each point in that trace—but there is also a ton of interpretation of that state that the programmer needs to do while debugging: “what does this sequence of events imply?” - “why is this pointer pointing here?” - etc.

Doing that interpretation is much easier when the programmer gets to selectively view that (again, HUGE amount of) hidden state. Print debugging gives the programmer complete control over what state is shown. No other debugger does that: they all show a ton of data and context (often useful!) and make certain operations easy (inspecting single variables! viewing call stack snapshot!), and these are often just the right things.

But sometimes they’re not. And often, when you start debugging, you don’t know if the fancy debuggers will be too much or not enough.

Print debugging gives you the power to write code to selectively view the (again, HUGE!) hidden state of your program, and this scales from the smallest code-tracing bug to the largest distributed systems.

Step debuggers, on the other hand, are essentially “no-code” debuggers — extremely useful for the purpose for which they are designed, still useful for adjacent purposes, and a great place to start if you know the tool well, but ultimately not as powerful if your needs exceed their capacities.

A good programmer will know how to use all these tools for what they’re best at!

If you've chased heap corruption printf doesn't really help you much but a data breakpoint is a godsend.

Same thing with watch windows, memory views and the like. There are classes of problems that do well with printf but calling them "no-code" is vastly underselling them.

I’m not sure when no-code became a pejorative, but that wasn’t my intent! Only that most of these tools, unlike print, are special-purpose, exceptional for that purpose, and often even useful in other circumstances.

A data breakpoint is a great example of something useful that print doesn’t do well.

You seemed to be drawing a parallel between no-code -> "not as powerful", in my experience they're different tools for different use cases.

I also don't think they're nearly as no-code as you call out. VS' watch window has very few limitations compared to printf back when I was working on win32 things.

Also important to consider iteration time. I once worked on a system where adding a printf was a 20 minute process due to the need to heavily optimize for a memory constrained platform(scripting fit in 400kb block with the asset bake step).

> in my experience they're different tools for different use cases

Exactly!

Debuggers are very useful tools, and typically not as general-purpose as print. I don’t view “not as powerful” as a meaningful distinction, because it requires that you ask “powerful at what?” ---

VS’ watch window is great but (I assume) doesn’t work across distributed systems, etc. — as a general technique, print is universal in the sense that there are very few problems that can’t be diagnosed by modifying your code and printing some (possibly a manipulation!) of the hidden state. This is going to be harder than using a special-purpose tool designed for exactly your problem.

In the same way, “no-code” tools are typically better and/or easier than writing code to solve the same problem, but special-purpose.

> typically not as general-purpose as print

In my domain which doesn't usually cover distributed systems printf can be worse because it introduces synchronization primitives that have caused race conditions to disappear(and that race condition causes second order heap corruption or the like). On one platform system memory was so small(8mb total) that each output to stdout went over the serial link slowing performance down to 1/20th of a realtime process under any real logging.

Like I said, different tools for different uses, and really depends on the context. If there was one size fits all then we'd just use that but the diversity of debugging tools I think shows that you need a variety of techniques to approach the problems we encounter.

> Like I said, different tools for different uses, and really depends on the context

We totally agree, and I'm not sure what we're arguing about--perhaps you can fill me in.

I'm arguing that print is almost always worse than any specialized tool. (After all, who would use a specialized tool worse than print?) There is not a one-size-fits-all tool, and print is not a one-size-fits-all tool.

Indeed almost every seasoned developer has a story about print failing. Whether it's the mysterious "Heisenbug" that disappears when you measure it (like the sync issues you mention) -- my personal story is when I was trying to debug a (class project) kernel scheduler. Printing to the console was so slow that by the time I'd printed anything, the scheduler had moved on to the next time slice!

It's worth nothing that "print debugging" is not literally just using the "print" function; it's a style of debugging that involves logging specific information using some logging function (usually, but not always, print) and then analyzing it after the fact (usually, but not always, by reading the printed output).

This strategy of "get data out, then analyze it" is the general form of print debugging, and in the small-memory case, or the sync Heisenbug case, this often means collecting data in the appropriate variables before outputting it to be visible. Isn't this still print debugging, even though it doesn't use a "print" function?

I think we're mostly arguing about how useful the various approaches are. At least for me print debugging is a measure of last resort unless I want to extract some historical data out and I know it won't influence the timing of the issue I'm trying to chase down.

With print debugging your inserting the whole build + deploy + repro setup loop into your debugging, if that's a long time(say 20 minutes in one job I had with production hardware) you're in for a world of pain. I find that just about any other tool usually is an order of magnitude more efficient.

Also even the "step debugger" tools do the same thing you'd do with a print. LLVM for instance uses the IR JIT API to generate watch/eval values: https://releases.llvm.org/9.0.0/docs/ORCv2.html#use-cases

IMO you should relentlessly optimize your iteration times, that's the inner loop of development speed and print debugging fares pretty poorly in that area for all the reasons above.

> I think we're mostly arguing about how useful the various approaches are.

Ah, that's fair.

> At least for me print debugging is a measure of last resort

Right, and I think this depends on the domain. For lots of mature environments, this makes sense -- there's been years for tooling to catch up to the kinds of bugs people run into, possibly corporate money being put into developing debugging tools, etc.

> IMO you should relentlessly optimize your iteration times, that's the inner loop of development speed and[...]

Agreed, though the effect on print debugging on iteration time is very environment-dependent.

> [...]print debugging fares pretty poorly in that area for all the reasons above

Adding console.log to a web app can be a trivial change (though of course reproducing app state is another issue) -- again very environment-dependent.

This is a lot of words but I wonder if you've ever worked with a debugger with watchable variables or immediate mode code execution. I find it odd you say print debugging is more flexible.
Using a debugger is largely passive - it shows you what is actually happening.

Debugging via print allows you to step outside and peer in ie it is active.

Print debugging can be prone to bugs within itself which may cause additional ignorance about the potential bugs being diagnosed. How meta can you get? 8) There's also the effect of the effort of actually looking - that may or may not have an effect.

Anyway, the discussion here is largely ignorant of language and function. At the moment I spend time fiddling up Python and OpenSCAD scripts if I dig out a programming language. For me, print is really handy. For a Linux low level latency sensitive driver in highly optimised ASM n C I suspect this matter is moot.

(most) debuggers can print - I often use conditional breakpoints with the "condition" "print(thing)". It works great, doesn't require re-compiling, can be enabled/disabled with a single click, etc. It's handy when you want to see a lengthy sequence all at once.
Discovering this capability changed how I debug. Conditional breakpoints that log-only creating an always useful, easily enabled/disabled log of critical method results without littering the code itself with logging statements.
With interactive debuggers like python's pdb you can actively manipulate data, print it out, basically do whatever you need to do in a REPL. It is way more effective than adding print statements, rerunning your script, etc. That being said I typically use a combination of both: I use a print to get me to where I think the problem begins, then an interactive debugger (pdb.set_trace / breakpoint / etc) to drill down into the details.
I have. What information exactly can you get from a debugger with watchable variables and immediate mode code execution, that you can't get from print?

I'm not making the argument that people shouldn't use debuggers -- obviously if there's a good one that does what you need, it'd be silly not to use it. And good debuggers are great.

But what happens when you're working on a distributed system? Or multiple processes on a single system? Or...etc. etc. -- debuggers are almost always built to support a particular set of use cases for a particular set of domains. Step outside, and they're not usually as useful.

print always works. It's almost never the best. It's more flexible because it can almost always be used.

> What information exactly can you get from a debugger with watchable variables and immediate mode code execution, that you can't get from print?

If you have some completely undocumented object oriented code with tons of relationships, peeling the state apart in a debugger is a good way to get an idea of what you even have that might be worth printing.

Silly example i made up on the spot, but not too far from real life. I'm iterating a list of 10000+ objects with about 20 or more nested properties. Some times one of them behave strange. This function runs several times per second.

Option1: Print all of them, requires rebuild, would log 200000 lines every second. Unless i wrap the print inside conditions, requiring yet another rebuild.

Option2: Conditional breakpoint: user[i].department.balance <= expected_value. Bang, i can now inspect both the complete nested user-object, and the previous/next item in the list, other local and global variables, the call stack of how it reached there, the state of all other threads in this moment, and so on. With the really good debuggers like .NET or JVM I'm even able to rewind the execution pointer to the start of the function or hot-swap the code as it's running.

In short, a debugger allows you to see all state of the program at once, and you can retroactivelly choose what is relevant. As opposed to printf where you must select upfront. Maybe even more importantly it's lacking stack traces, unless you add a log and request-id at every function-enter/exit.

There is also the case of third party code which you can't edit to add printf to. Many environments usually by default give you pretty good context of the symbol names or even complete source code which you can step through (except c++ land where you'd need a debug-build of the lib, which isn't impossible either).

(comment deleted)
>What information exactly can you get from a debugger with watchable variables and immediate mode code execution, that you can't get from print?

Lots of things but an obvious example is private member info.

> it lets the programmer customize their view of the program’s (very, very large) hidden state in any way imaginable.

I’ve been trying to articulate this to myself for a long time. Thank you.

That suggests that debuggers should let you write visualizers and they should be so easy to write that you don't hesitate to write them.

That seems to be the idea behind Glamorous Toolkit https://gtoolkit.com/

I'd be curious what it would take to add any of those concepts to existing debuggers for other languages.

"Print debugging always works..."

Nope, print debugging does not always work. Please stop saying that. Sure, it's often useful and often works. But go try to debug a race condition, or bug in some locking mechanism with print statements. I'll wait as you add a bunch of print statements, and then learn the harsh lesson that you are now debugging a completely different program with very different performance characteristics that now doesn't deadlock where it used to deadlock because you slowed down one of the threads massively as it spends time it used to not spend logging stuff.

"Step debuggers, on the other hand, are essentially “no-code” debuggers — extremely useful for the purpose for which they are designed, still useful for adjacent purposes, and a great place to start if you know the tool well, but ultimately not as powerful if your needs exceed their capacities."

I guess you'd think this if you thought print debugging "always works". But I promise you it is equally (if not easier) to exceed the capabilities of print statements in some scenarios.

Debuggers don't work in the situation you described either.
Yes, my reaction entirely, I do a lot of real-time work, an interactive debugger is quite useless when stopping everything is not an option
Where did I say that they did work?
You don't put printf() in critical sections, instead you collect some statistics. Once critical section is over you can freely print collected data for further analyzis. Or you can print it once a second. This simple technique works well for debugging racing conditions, performance issues (profiling), memory leaks, kernel drivers and bare-metal stuff where timing is a concern. So, yes "Print debugging always works...", but must be used wisely. :-)
Collecting statistics can change race conditions too, it's just less likely
> A good programmer will know how to use all these tools for what they’re best at!

Yeah, I run through valgrind; 9 times out of 10 I don't even get to the point of starting the debugger or recompiling with prints. The remaining 1 time where valgrind output is clean, I go with printing. About half the time the prints aren't enough and then I fire up the debugger.

Also, it depends. I'm working on a legacy C# project where the previous dev read somewhere that passwords shouldn't be stored in plain-text, so when changing the stored DB credentials you have to set a breakpoint where the hash is calculated, change the variable holding the cleartext password to the new password you want to use, step the line that calculates the new hash, copy the text out of the watch window and finally paste that text into the file holding the credentials.

I do not know how this helps. I also don't care enough to write a c/line utility that generates the hash in a base-64 string, becasue we've changed the DB credentials for the webapp only once since he left. We may have changed it more often if we had an easy way to do so :-/

Worst thing with Java is, especially when taking over a project or extending some open source thing: finding where that goddamn log4j config is. Is it in web-inf, in tomcat/glassfish config, somewhere entirely else (e.g. specified in the run config), or is it configured in one of the five wrapper layers.

And then you have to figure out the syntax... does it want package names, class names, or (hello Intellij plugins!) need a fucking # at the beginning to be recognized.

And then you have stuff like a "helpful" IDE that by default only shows WARN and above levels without telling you somewhere "there might be stuff you don't see" like Chrome does.

For actual debuggers, shit is worse, across the board. Running in Docker is always a recipe for issues, not to mention many applications actively messing around with stuff like ports.

A system.out.println always ends up somewhere sane.

Especially when you need to run your code on a remote server as part of a bigger platform like some Cloud or Serverless system.

These systems likely already have a way to get logs, but good luck getting a debugger to work there.

> Personally, I think my biggest reason for using print debugging is.. it works.

I agree.

A similar statement is that using a debugger often does not work.

the sort of ratholes I've run into are: - debug build - proper symbols - interrupts and debugger - kernel and debugger - unfamiliarity with debugger - limitations of debugger

Without a proper debug build, you can't run the debugger effectively. You have to set up a whole debug environment.

Sometimes you need to do broad work to get proper symbols and stack trace information. By broad, I mean a debug build for everything.

I've also found many debug builds, apart from altering some behavior, they also turn on a lot of printfs.

If you're using interrupts/timers or debugging a kernel module, many times debuggers don't work or may alter, move or supress the problem.

and then there's the... I haven't used a debugger in 6 months, how do I do (very simple thing). And sometimes the debugger is just not the right tool or a tedious tool to use. "If I just load this one macro and somehow get the right address maybe I can decode this one kernel data structure..."

Personally, many times an ephereral printf isn't crude and meaningless, it's a precise scalpel getting to the core of the problem.

This is the only argument for print debugging I can understand. Given the debugger experience I've had at every job in the last 10 years (IDE-driven, set breakpoints in editor, inspect state visually) it's mind-boggling that anyone thinks print statements are superior, but getting that experience can be frustrating depending on your tools and environment.

FWIW my experience debugging Python in VS Code required no setup and I've encountered zero issues.

printf debugging always have a place, but for some reason, I found the debugging experience to be worse than 20 years ago. Tools like Visual Studio still have great debuggers, but I didn't notice significant improvement since the early days, and newer toolchains are worse.

A couple of years ago, I had to maintain a bit of Java code using Eclipse. That is, the old IDE everyone loves to hate. And while some of that hate is well deserved, for debugging, it was the most pleasant experience I had in a long time. Nice object inspector, edit-and-continue, conditional breakpoints, and step-by-step that works. Much better than fumbling around with GDB or one of its less-than-perfect UIs.

Also note that printf debugging and the step-by-step and breakpoint kind are not mutually exclusive. With an edit-and-continue feature, you can get the best of both worlds, but that's not something common these days, unfortunately.

Maybe it was because I was exposed to it early in my career but I have yet to find anything that rivals Visual Studio debugging, either from a "just works" perspective or ability to deep-dive into gnarly memory corruption(memory windows, robust watch windows and data breakpoints).
A lot of debuggers will also print/log and can even inject those statements into a running app where hot reloading manual print statements would otherwise not work.

From there there are situations where a debugger will save a LOT of time. I'm thinking of trying to figure out what's causing a behavior in a large dependency injected application with plugins when you have little to no familiarity with all the code involved. And then of course all the other things a debugger can do for you.

> Clearly Real Debuggers offer a superior experience to print debugging in so many ways. But print debugging is just easier to get started with, and it reliably works anywhere, so that’s why we use print debugging so much.

I think the tone of the first sentence and the word "superior" unnecessarily creates a strawman.

The beauty of printf debugging for a novice C programmer is that the recompiling with printfs changes the memory layout so your buffer overflow no longer segfaults you.

ALternatively, your printf can use the wrong formatter string, and cause unrelated crashes. Such joy!

Makes me nostalgic for the good old days.

> ALternatively, your printf can use the wrong formatter string, and cause unrelated crashes. Such joy!

What compiler are you using? Aztec C? Prehistoric C?

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.

A few more reasons why print debugging is used. If you are debugging multiple things at once, you’ll have breakpoints set that aren’t necessarily needed at the moment, meaning you have to continue a bunch of times to get to the right spot. Or your breakpoint needs to be in a loop that is called multiple times and conditional breakpoints are a pain and subject to code errors in the condition itself. Many debuggers are not great at examining state of objects, for instance a deeply nested object for which you want array index 42 within a dictionary of an object. Or you need to see a value that is calculated rather than just present in the current state.
> you’ll have breakpoints set that aren’t necessarily needed at the moment, meaning you have to continue a bunch of times to get to the right spot.

Python: if something: breakpoint()

Js: if (something) debugger;

Much easier than breakpoint conditions in visual debuggers imho.

That runs the risk of forgetting to remove it.
If you don't read your commits before pushing or even merging them ... But I use `git add -p` and `git checkout -p` which works well against this
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.
Sounds a lot like syntactic sugar or a DSL for symbolic breakpoints combined with conditionals. That's certainly doable.

Something like: func1(4) > func2(null) debug;

Semantically: upon func1 called with arg 4 and some descending path that calls func2 with arg null, enter the debugger

Neat idea!

I got the idea when I was thinking about the applicability of computer algebra systems to math education. Some way of visualizing the decisions and steps of a logically complicated program seemed necessary for that. Getting a readable trace of the computation in a similar way to the one that some logical programs or expert systems can justify their reasoning with seemed like a usable form of such a visualization, and some time later then the analogy with CSS/XSLT struck me. I was thinking of collecting all the steps into an output, but setting breakpoints in a similar fashion with individual "selectors" could be useful for debugging, too.
For production, the only way is logs (print debugging).
GPU equivalent: putting stuff into vertex buffers so you can inspect the buffer and see if the expected values are there.
Print debugging is great but you'll pry the IntelliJ debugger from my cold dead hands.
Completely agree. When implementing new functionality in my Kotlin Spring Boot apps, I find the debugger crucial for fixing any exception that isn’t immediately clear. I’ll simply rerun my test with a breakpoint on the failing line, peruse the values of local variables (often spelunking deep into nested objects), and test theories with the window that lets my evaluate arbitrary expressions. Occasionally, I’ll change the value of a local variable and let the program continue to see if that value would fix the issue.

It’s a workflow that makes “Wait, why did that happen” such an easy question to answer.

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]

You call it print debugging, I call it a powerful experimental framework for testing hypotheses about the behaviour of the program.
I hate coding in an environment that does not easily support step-wise debugging. And yet, I use printf 10x-100x more frequently. Printf is actually causing you to do some thinking, and writing a little bit of code to conduct an experiment that hopefully will tell you in one shot what the problem is on a simple run. Step-wise debugging instead forces you to think about the problem, but then go through carefully and run a lot of mental load at each "next step" push to figure it out.

That being said, there's almost no good reason for a platform to not support step-wise debugging, so it's a big code smell that you're going to have a bad time in general there (even if in practice you'd largely use printf anyway).

There are environments where printf is not possible - e.g. MCU development. For instance, if the code breaks before the serial port is setup for printf to work.
You can always just send the output to a block of memory that is reserved for debugging, and dump out that block when necessary.