I'm a Vim person, and am envious of IDE users with integrated breakpoints, stepping, and variable inspection with just a mouse, but only today found out about Vim's `termdebug` :headdesk:
So I'm wondering... what does the HN crowd use to investigate code problems:
- sprinkling print
- IDE with integrated debugger
- termdebug (i.e editor with gdb in another pane)
- debugger by hand on the command line
- other (please comment)
A traditional forwards-in-time debugger like gdb sometimes beats print debugging, but often not or not by much. OTOH a reverse-execution-capable debugger like https://rr-project.org (disclaimer: I'm a maintainer) enables much more powerful debugging strategies like "set a data watchpoint and reverse-continue to where the value was written", which trumps print-debugging strategies in many cases (e.g. memory corruption). I use rr a lot to debug my main project, Pernosco. FWIW Pernosco is written in Rust, and it does have a fair bit of logging built into it, but I still find rr very useful there.
Pernosco itself is a new kind of debugger (see https://pernos.co --- let me not be uncouth by promoting it too hard here). I don't use Pernosco to debug itself that much, partly because that's challenging for technical reasons, but also because it is certainly true that debugging tools are more valuable for large complicated projects you don't fully understand and Pernosco is not such a project (yet).
For debugging proper, sometimes it is indeed faster to just use print statements. As with everything else, you have to use your judgement.
If you are in a situation where you would like to inspect the flow of control through a large chunk of code, debugger is the best. How many print statements are you going to write? Twenty? Hundreds? Okay the log shows that some invariant somewhere broke, but now what? How did we actually get here? Of course, at some point using the debugger makes more sense in such a scenario.
The biggest problem I have seen people have with debugger (in C++ world) is that the debug builds run much slower and it can sometime takes hours to reproduce the state you want to debug. The tip I usually give is: you don't have to have the whole project be built without optimizations. Just enable those flags for the specific components you are debugging.
I read the code and think through it to form a hypothesis, then use print/error logging to test the hypothesis and come up with a fix.
I haven't used a real debugger in a long time.
Another problem is that often the debug builds of things at work run too slow to be practical, or they're too big to fit on the target device. My use of debugger at work usually boils down to getting and translating stack traces from core dumps we receive (on command line). There is a very rare occasion that I can do something on my local machine and then I use a debugger. Life is too short for 15-45 minute add logs + compile cycles. I also use bpftrace whenever I can.
At home I write Rust, but IME templates don't play well with the debugger, so I usually fall back on prints and bpftrace again.
But generally I'm convinced that using debuggers could be a big productivity boost. Here[1] you can see a demonstration of how smooth working with a debugger can be, in this case it's RemedyBG. But forget about this level of comfort on Linux. The whole video may be also worth watching if you like rants and want to see how Visual Studio debugger quality dipped over the years (first the current VS debugger is shown, and then an old version (from VS6 IIRC)).
My build system automatically rebuilds my code and reruns all tests immediately upon me saving a file. So I sit with the test output open in one window and the code in another. I add a print statement, save, and a few seconds later, I see it in my output log. No need to even move focus out of my editor frame.
I find that for most purposes, this is much faster and more powerful than trying to use a debugger. I don't have to switch workflows between code editing and debugging. I get to probe the state from anywhere in the execution timeline, not just one breakpoint at a time.
Of course, this requires that whatever I'm debugging is reproducible in an automated test that runs fast (within seconds). This approach doesn't work nearly as well for anything that requires interacting with the program. But I mostly work on lower-level infrastructure and libraries that are highly test-driven and non-interactive, so it works out.
A few superficial things that help this workflow:
- I have a dedicated macro, KJ_DBG(), for debug prints, so I can easily stop myself from accidentally committing debug prints.
- The macro is optimized for debugging. If I write `KJ_DBG(x, y, z)` I'll get a log message saying "x = - `KJ_DBG(kj::getStackTrace())` dumps the call stack trace, `kj::getAsyncTrace()` traces async event callbacks, etc.
Mainly I use the 'xlog' function. I can then wander about the logging file (using a text-editor for navigation) looking at various 'befores' and 'afters'.
I use a bit-mapped variable 'debug' which allows or disallows the logging statements accordingly.
For example, in an emulator, with debug only configured for floppy-disk-controller logging, and looking at the floppy-disk controller code, we might see:
xlog (FDC,
"sdma_getdatareg: disk_data: fd_byte_ptr:%04X bytes_to_xfer= %4X data:%02X\n",
fdcd->fd_byte_ptr, fdcd->bytes_to_xfer, data);
which would be logged, and xlog (INFO, "RESETTING %s\n", MACHINE_NAME);
which would not.As for using 'gdb', I tend to use that only when I have no clue, or a wrong clue, where the code is breaking down.
I tend to leave the logging code around if it's not particularly obstructive or long-winded as I can switch it out at will by setting 'debug' to zero.
IDE with integrated debugger for larger programs (mostly C# and some Java experience here).
gdb (or similar) for programs without an IDE and larger than a small one-off program.
Some languages also lend themselves (or their implementations do) to better debugging experiences. Common Lisp debugging, for instance, is almost a joy. For grins, I implemented the VM for the Synacor Challenge [0] in tandem with SBCL's debugger. After parsing the opcode, the program entered an ecase. If there was no entry for the opcode, it brought up the debugger, I extended the program, recompiled whatever needed to be recompiled, and resumed it. Wash, rinse, repeat. Once every opcode was implemented it then went on to run the image and I could finish the challenge.
E.g. you hit an assertion in your code because some value was outside the expected range? Fine, set a watchpoint on it when the assertion fires, and simply reverse-continue to see where that value came from. It could change your debugging life...!
Many others champion “you should understand your own code well enough that you don’t need a debugger”. Those people are usually in an unusual situation where they can work on a small bit of code in a small/solo team for a very long time.
Meanwhile, much of my career has been spent stepping through code I’ve never seen before and will never see again trying to fix bugs that span multiple teams and external libraries.
Sometimes it takes hours, sure, but I'm not seeing how a debugger would accelerate that (since in those cases I have to wrangle a lot of external state like DBs and VMs/containers). Debuggers are a pain to learn and utilize to their full capability.
Am I missing out? I'm not saying that I'm not but haven't practically felt the actual need for a debugger in a long time.
One particular knotty routine for me was a small synth, I knew I had neither the math ability or c wizardry to develop complex things easily so I made sure there was plenty of scaffolding to assist me. For each 1k of audio output, there is easily 1m of debugging output that I would then "query" with perl, pagenate each step that I could page through, etc, debugging was put in from the start.
Allowing flexible debugging strategies that I could adjust for the difficulties at hand just seems right for me and I find often that different programming constructs need different debugging strategies, and I just don't think debuggers are agile enough to let me adjust.
Its really that I'm just too dumb to get benefits from debuggers, although I'm guessing they may be just perfect for folks smarter than me.
I use debug print statements to monitor running programs while doing end to end tests.
I have functions to log errors and restart programs in production. Any busfault gets trapped and logged as well. Often just knowing which line of code barfed tells you what happened. Often I also log the calling address off the stack.
I also have a table driven command line interface that I use heavily to poke at and inspect running programs. Having that probably reduces my need for a debugger. Unlike a debugger it's effect on a running program is minimal.
For timing related stuff and debugging bus interfaces I often use a four channel scope.
When I started writing Rust some years ago I switched to using an IDE full time. Before that I had mostly been using Vim for the majority of the code that I wrote.
The IDE I use is JetBrains CLion, and I love it a lot.
Importantly, JetBrains IDEs have a Vim emulation plugin that you can install, so you still get to use many of the key bindings and features that you know and love from Vim, including recording macros and playing the macros back etc.
I really recommend that you check out the JetBrains IDEs. Install a trial version of the one(s) suitable for the language(s) that you use, install the Vim emulation plugin, and use it for a while.
For low level code where you really want to see all the program/function state evolving over time, they can be useful.
Your Java program throws some random NullPointerException? Prints, and lots of them. The experience with the debugger is miserable. I set a breakpoint somewhere, then do I jump or step or whatever? Who knows, all I know is whichever I pick is the wrong one everytime.
I saw another poster mention a debugger that lets you put breakpoints that print stuff. Now that would make me use a debugger all the time. Maybe the debuggers I have used already support that, I certainly wouldn't know if they do, they didn't make it discoverable.
My debugging tooling of choice is `console.log` when dealing with simple flows: Do an HTTP request and print the value of a variable
I find it lighter (and faster) than attaching a debugger and slowing everything to a halt.
For advanced asynchronous behavior I use Chrome Dev Tools + React Developer Tools + Redux DevTools with these I can inspect any value, profile, time-travel etc..
I'd say the ratio is 80% console.log / 20% dev-tools
The experience is even nicer in the Pycharm IDE, where I can set breakpoints with arbitrary conditional logic just by pointing and clicking. It's so easy it almost feels like cheating.
That said, Pycharm is a heavy beast. Psychologically it's not worth the overhead of opening it or even switching projects within it unless I'm working on something involved for an extended period of time. So figuring out how to do something like this in Neovim has been on my to-do list for a while.
I also use the built-in Python logging framework to help me identify where problems might be happening in the first place.
- In C++, setting up a (visual) debugger is often non-trivial, and the quality of debugging is naturally lower than in higher-level languages. So I tend to get along with print. Some bugs absolutely require the debugger though.
- With WASM I tried to get the Chrome debugger to function but gave up. Not sure it works yet.
- In Python, usually I am executing small scripts via the command line, outside an IDE, so I just resort to print statements.
To be clear none of these are my primary programming language. If I was spending significant amounts of time in front of a language I would give JetBrains however much money they asked for to get a real IDE and debugger.
Generally I don't find command-based debuggers to be that compelling, it feels like writing text with `ed`. When I do take the time to set up a visual debugger it is a significant productivity multiplier.
My technique for debugging with Clojure + emacs was to use a bunch of print statements and then also using a repl to try and duplicate the issues or at least see what was going on. Less than a month ago I started using the CIDER debugger inside emacs and have been happy with it overall. There are some issues I have had, namely that you can't set breakpoints in macros which is understandable. I have gotten around that by creating a temporary function and move all the code into that function. The main use case I have for doing that is when trying to debug Compojure routes, which are defined in macros.
However, over the last few years I've switched mostly to CLion, which has excellent support for gdb and gdbserver, so you can use it to debug local, remote and embedded applications directly on the target with a debug probe. It can't be understated just how much of a timesaver a graphical debugger is compared to controlling things through the CLI. Stepping in/out/over, setting breakpoints and watchpoints, viewing the call stack and individual frames, and jumping back and forth from source code to disassembly. You can do all this from the CLI, of course, but it's much less efficient.
As much as I despise Eclipse, even it manages to have excellent gdb and gdbserver support which is also vastly better than the CLI for debugging.
Also, with a good UI debugger there's not much to "learn" since the UI is pretty much self-explanatory. You usually have two or three hotkeys for step-into, step-over and step-out, click with the mouse to set a breakpoint, and a few UI panels to inspect variables and memory. Everything else are advanced features that are useful to know, but not essential.
I am currently working on a Python/Django project of around 45k lines of code, 28k of which are tests. For me, this approach works quite well. It is not that I am against debugging or that I don't value debugging as a tool, but I just never feel the need to use one.
The tests also have a nice side-effect: because I do TDD a lot, I mostly end up with a lot of small understandable units, so I am effectively preventing code with properties that need me to follow each statement one by one.
I hardly ever feel the need to interrupt the running process to inspect the current variables. In many cases, this would be futile anyway, because I started writing a lot of generators.
I’ll also use print debugging if I feel the situation isn’t worth bringing in a debugger, primarily one I don’t know well yet (see above).
On the other hand, when I'm working with a system that I don't know inside and out, a debugger is immensely valuable for helping me 'navigate'; to see what actually gets called when some interface/virtual method is called.
I live in the second world more often than the first.
When faced with a new API or algorithm, I write small programs with lots of prints to test/understand the function/object in question. Once that's done I encapsulate as a module and retain some assertions to catch any abuses.
I then build systems using the pre-tested modules. At that point I rely on the assertions, logging and exceptions to identify the source of problems. Problems are quickly traced back to the module with bugs which then undergoes further polishing as above.
I've written code to print scripting language stack traces when native code segfaults.
I've written code to auto-collect crashdumps from coworkers, and add important globals (or what they point to) to said crashdumps when collecting full 32GB+ memory dumps are impractical.
In-process debug toggles might show graphs or annotations that provide much more intuitive and visual alternatives to printf spam.
Unit and fuzz tests help catch things before they manifest in the wild.
GDB/LLDB supports python scripts which let you customize the debug view of some of your more opaque types. MSVC has .natvis files, which despite being awkward XML soup, can also be quite useful.
The cost of working out how to configure the IDE/debugger to set the environment and run the build commands was never worth it. But for debugging tests it’s great.
I think a working debugger is faster than adding print statements, but slower than debugging by inspection (ie running the code in your head). So I read the code and if I can’t work it out, I’ll add print statements, and if I’m still confused then I’ll work out how to set up the debugger for my environment, again.
But because there are two steps before I’ll break out the debugger, I don’t use it too often.
If I have no freaking clue why something is going on, I run through a debugger line by line and watch it all. But in all honesty, that is a rare case. It is far more likely that just slowing down and thinking what could possibly cause a result will guide you in the right direction.
Before that--almost twenty-five years ago--I figured out how to use the COBOL "animator" to find out why Peoplesoft payroll processing was crashing on us. There was no way in the world to have figured that out without the animator.
These days, though, I mostly use print or logging.debug
Leveraging DAP lets you have a uniform interface for debugging in multiple languages. (DAP is a sister protocol to the Language Server Protocol which abstracts IDEs from debugger services)
Usually if I need to break out the debugger, the flow has already become out of control in complexity to the point where print doesnt even help anymore and needs a refactor anyway, but the bug needs to be fixed sooner than the 1-2 weeks that would take. I also tend to find flaky test fixes faster using the debugger.
2: I wish, but most of the time i can't run in on my company laptop, so no.
3: Yes, pdb/gdb every time
4: With lisp (but really, it's like a step-by-step debugger, so no?)
If it's available I would question not using one. If I was pairing with someone who wouldn't use one or didn't know how I would be frustrated.
When I go back to frontend code, I will definitely use a debugger in the browser, unless it is a super heavy client riddled with performance issues (and even then, probably).
Later I was the test manager of Turbo Debugger at Borland.
Today I almost never use a debugger. It is my absolute last resort. I print to the console, though.
Personally I think most people never get taught debugging tools properly, so they get to use like 10% of their capabilities and then think down of debuggers.
Visual Studio team has lots of tickets where asking for feature X, they get closed with already available.
Most times either a print statement or the equivalent of an assertion will be enough to spot the problem.
Some stuff need to be coded in python though, and then sometimes I use pdb.
If you’re writing code that you yourself don’t understand then a debugger isn’t going to get you much, except maybe if you’re using it as a kind of imitation REPL to learn the language.
If you’re dealing with code that someone else wrote then it can be a bit more useful, since with large codebases it’s not always practical to fully understand everything. However, it’s liable to give you a false sense of understanding, so some humility is warranted.
In fact, I want to make the point more strongly. Operationally reasoning about program behavior by stepping through a specific execution is like doing analysis using a jar and a pile of pebbles. It will work in the simpler cases, but is utterly useless for nontrivial algorithms, including any that involve concurrency or nondeterminism.
Low level code? All the time, and only at assembly level (even if I am debugging C).
(Professional coder since 1987.)
If anyone is interested gdb has a visual mode. Eg: gdb ./a.out
then while inside gdb ctrl+x, a
so press ctrl+x at the same time, let go and press button 'a'
My entire development stack is installed on any machine that runs chrome. It's awesome.