While perhaps this is true of some sort of junior developer, I have both written my own debuggers and still lean heaviest on print debugging. It's trivially reproducible, incurs basically zero mental overhead, and can be reviewed by another person. Any day I break out a debugger is a bleak day indeed.
Profilers are much easier to argue for as it is very difficult for one to produce equivalent results without also producing something that looks an awful lot like a profiler. But in most cases the mechanisms you mention are just straight unnecessary and are mostly a distraction from a successful debugging session.
Edit: in addition to agreeing with a sibling comment that suggests different problems naturally lend themselves more to debugging (eg when writing low-level code a debugger is difficult to replace), I'd also like to suggest a third option languages can take: excellent runtime debugging ala lisp conditions. If you don't have to unwind a stack to catch an exception, if in fact you can modify the runtime context at runtime and resume execution, you quickly realize the best of both worlds without having to maintain an often extremely complex tool that replicates an astonishing amount of the language itself, often imperfectly.
When debugging parsers for my toy programming languages print debugging is less helpful and I make heavy use of all the debug tools you mention. The same goes for most types of business logic—writing a test and stepping through it in the debugger is usually the way to go.
But when troubleshooting odd behavior in a complex web app, the inverse is true—there are usually many possible points where the failure could occur, and many layers of function calls and API calls to check, which means that sticking a debug statement prematurely slows down your troubleshooting a lot. It's better to sprinkle logs everywhere, trigger the unexpected behavior, and then skim the logs to see where things stop making sense.
In general I think there are two conditions that make the difference between the debugger or print being better:
* Do you already know which unit is failing?
* Is there concurrency involved?
If you don't yet know the failing unit and/or the failing part of the code is concurrent, the debugger will not help you as much as logs will. You could use logs to narrow down the surface area until you know where the failure is and you've eliminated concurrency, but you shouldn't jump straight to the debugger.
e.g. LOG(INFO_LEVEL, "Service startup") and printf("Here11") are completely different situations.
Indeed, the very submission is arguing for printf style debugging instead of logging. Like it uses it as the alternative.
Real-world projects should have logging. It should have configurable logging levels such that a failing project in the wild can be configured to a higher logging level and you can gather up a myriad of logs from a massive, heterogenous cross-runtimes and platforms project and trace through to figure out where things went awry. But that isn't print debugging or the subject of this discussion.
Yeah, this is a bogus distinction they're drawing. Logging and printf style debugging are the same thing at different phases of the software lifecycle, which means they can't be alternatives to each other because they can't exist in the same space at the same time.
As soon as your printfs are deployed to prod, they become (bad) logs, and conversely your "printf debugging" may very well actually use your log library, not printf itself.
Last time I tried it you were able to add logging statements "after the fact" (i.e. after reproducing the bug) and see what they would have printed. I believe they also have the ability to act like a conventional debugger.
I think they're changing some aspects of their business model but the core record / replay tech is really cool.
1 - If your parsers are not pure, you either have a very weird application or should change that.
Of course, I pair this with a modern emulator for the target platform where I can at least see my disassembly, set breakpoints, watch memory values. But when I'm working on some issue that I can only reproduce on hardware, we get to bust out all the fun manual toys, because I just don't have anything else available. On the very worst days, it's "run this routine to crash on purpose and paint the screen pink. Okay, how far into the code do we get before that stops happening? Move the crash handler forward and search. (Each time we do this we are flashing an eeprom and socketing that into the board again.)"
Also suspending a thread to peek around is more likely to hide timing bugs than the extra time spent doing IO to print.
Something is broken in prod, you cannot reproduce it in your test environment because you think it may be due to a config (some signing keys maybe) you can't check. And it looks like someone forgot to put logs around whatever is the problem.
You can either: spend multiple hours trying to reproduce and maybe find the cause. Or take 5mn, bash into one of your nodes, add some logging live and have a result right now: either you have your culprit or your hunch is false.
Remote debugging is a thing that exists.
I still print stuff plenty, but when the source of an issue is not immediately obvious I’m reaching for the debugger asap.
This does sound painful, but this is not what most people who advocate for print debugging are advocating for.
If I'm only going to add one print statement, that's obviously a place where a breakpoint would serve. When I do print debugging, it's precisely because I haven't narrowed down the problem that far yet—I may have ten theories, not one, so I need ten log statements to test all ten theories at the same time.
Print debugging is most useful when the incorrect behavior could be in one of many pieces of a complex system, and you can use it to rapidly narrow down which of those pieces is actually the culprit.
However, at least personally, I've also felt that there was a lot of truth to that Ken Thompson quote. Something along the lines of: "when your program has a bug, the first thing you should do is turn off the computer and think deeply."
Basically, a bug when where your mental model of the code has diverged from what you've actually written. I think about the symptoms I'm observing, and I try to reason about where in the code it could happen and what it could be.
The suggestion in the parent comment that I'm just too stupid to look into or learn about debuggers is so condescending and just plain wrong. I've looked into them, I know how to use them, I can use them when I want to. I simply tend not to, because they don't solve any problem that I have.
Also, the implication that I don't use completely unrelated tools like profilers is equally asinine. Debuggers and profilers are two completely different tools that solve completely different problems. I use profilers almost every day of my career because it solves an actual problem that I have.
If your insecurity leads you to misrepresent what someone actually said so disgustingly, maybe Hacker News isn't for you.
The scenario I gave is when there are performance problems with a developed project (you know -- where a profiler is actually usable, after they already decided on an approach and implemented code) and the developer is effectively guessing at the issues, doing iterative optimize-this-part then run and see if it's fixed pattern. This is folly 100% of the time. Yet it's a common pattern.
I like the solid approach described in Understanding Software Dynamics by Richard L. Sites
That said, on one project I did have a semi-decent experience with a debugger for PHP (couple of decades back) and when it worked - it was great. PHP didn't have much of a REPL then, though.
I use PyCharm for my projects including Python, for instance, and it has absolutely fantastic debugging facilities. I wouldn't want to use an IDE that lacked this ability, and my time and the projects are too valuable to go without. Similar debugging facilities are there for Lua, PHP, Typescript/JavaScript, and on and on. Debuggers can cross processes and even machines. Debuggers can walk through your stored procedures or queries executing on massive database systems.
Several times in this thread, and in the submission, people have referenced Brian Kernighan's preference for print versus debugging. He said it in 1979 (when there was basically an absence of automated debugging facilities), and he repeated it in an interview in 1999. This is used as an appeal to authority and I think it's just massively obsolete.
As someone who fought with debuggers in the year 2000, they were absolute dogshit. Resource limitations meant that using a debugging meant absolutely glacial runtimes and a high probability that everything would just crash into a heap dump. They were only usable for the tiniest toy projects and the simplest scenarios. As things got bigger it was back to printf("Here1111!").
That isn't the case anymore. My IDEs are awesomely comprehensive and capable. My machine has seemingly infinite processor headroom where even a 1000x slowdown in the runtime of something is entirely workable. And it has enough memory to effortlessly trace everything with ease. It's a new world, baby.
Is it real mind share? Is it bullshit?
Print debugging is the literal pocket knife of debugging.
And to be clear, print debugging and pervasive, configurable logging are very different things and the latter is hugely encouraged (even with logging levels), while the former is almost always suboptimal. Being able to have your client turn on "DEBUG" logging and send you the logs after some abnormal behaviour is supremely useful. Doing "prinftf("Here!")" in one's project is not, or at least not remotely as useful as better approaches.
It minimises the mental effort to get to the next potential clue. And programmers are naturally drawn to that because:
1. True focus is a limited resource, so it's usually a good strategy to do the mentally laziest thing at each stage if you're facing a hard problem.
2. It always feels like the next time might be it - the final clue.
But these can lead to a trap when you don't quickly converge on an answer and end up in a cycle of waiting for compilation repeatedly whilst not making progress.
not always; sometimes print debugging is much more time efficient due to the very slow runtime required to run compute-intensive programs in debug mode. I'll sometimes forego the debugger capabilities in order to get a quick answer.
Also, what's "modern" about "Walk the call stack! See the parameters and values, add watches, set conditional breakpoints"? Those are all things we had many decades ago (for some languages, at least). If anything, many modern debugging environments are fat and clunky, compared with some of the ones from way back when. What has greatly improved though are time travellers, because we didn't use to have the necessary amounts of memory lots of the time.
So please refrain from calling people with different preferences uneducated. [Ed. I retract this bit, though I think it is not unreasonable to associate lack of knowledge with lack of education (not necessarily formal education!) I don't want to quibble over semantics.]
OP said:
> … people who lean on print debugging often have incomplete knowledge of the immense power of modern debugging tools.
I am educated. I have a metric fuckton of incomplete knowledge in all areas of life.
You’re poking at something that wasn’t said.
(Well, other than my own conditional breakpoint features built into the code, doing things like programmatically trigger a breakpoint whenever an object with a specific address (that being settable in the debugger interactively) passes certain points in the garbage collector.)
We have successfully sorted out how to manage both loops, and set effective breakpoints to debug efficiently. We also log extensively.
I know it’s possible because I do it every day.
"Those are all things we had many decades ago"
I didn't claim this is some new invention, though. Though as someone who has been a heavy user of debuggers for DECADES, debuggers have dramatically improved in usability and scenarios where they are useful.
"So please refrain from calling people with different preferences uneducated."
But...I didn't. In fact I specifically noted that graduates of excellent CS programs often haven't experienced how great the debuggers in the platforms they target are.
We all have incomplete knowledge about a lot of things.
It's particularly annoying on projects that are set up without considering proper debuggers, because often it's impossible or difficult to use them, e.g. if your program is started via a complicated bash script or makefile rather than directly.
All the processes end up being debugging simultaneously in the same instance of the debugger, which I've found to make light work of certain types of annoying bug. You might need a mode where the timeouts are disabled though!
If you add a bunch of print statements every few lines, it's easier to run the code and see you got to the checkpoint line 1580 and 1587 but not 1601, than to have to manually click through a dozen breakpoints and note the last one you passed before the problem occurs.
If you have a "hard crash" where you can give a stack trace, that's less of a need, but often it's something like "this value was 125 when it entered module Foo and came back as 267". Monitoring the expression can sometimes help, but it might also be a red herring (we trap that it got set at the end of function Bar, but then we have to dig into function Bar to find the trace). Printfs can include whatever combination of values are worth reporting at any time.
Yes, any debugger can do all of that, but when trying to spin it up ad-hoc, printfs can be less hassle than trying to pull up the debugging tools and wire it up inside the IDE.
This isn't just an assumption I'm making: years of being in developer leadership roles, and then watching a couple of my own sons learning the practice, has shown me in hundreds of cases that if print-type debugging is seen, a session demonstrating how to use the debugger to its fullest will be a very rewarding effort. Even experienced developers from great CS programs sometimes are shocked to see what a debugger can do.
Walk the call stack! See the parameters and values, add watches, set conditional breakpoints to catch that infrequent situation? What! It remains eye opening again and again for people.
Not far behind is finding a peer trying to eyeball complexity to optimize, to show them the magic of profilers...