Skip to content(if available)orjump to list(if available)

Things you can do with a debugger but not with print debugging

bluishgreen

There are two kinds of bugs: the rare, tricky race conditions and the everyday “oh shucks” ones. The rare ones show up maybe 1% of the time—they demand a debugger, careful tracing, and detective work. The “oh shucks” kind where I am half sure what it is when I see the shape of the exception message from across the room - that is all the rest of the time. A simple print statement usually does the trick for this kind.

Leave us be. We know what we’re doing.

planb

The tricky race conditions are the ones you often don't see in the debugger, because stopping one thread makes the behavior deterministic. But that aside, for webapps I feel it's way easier to just set a breakpoint and stop to see a var's value instead of adding a print statement for it (just to find out that you also need to see the value of another var). So given you just always start in debugging mode, there's no downside if you have a good IDE.

ViscountPenguin

I've had far better luck print debugging tricky race conditions than using a debugger.

The only language where I've found a debugger particularly useful for race condition debugging is go, where it's a lot easier to synthetically trigger race conditions in my experience.

seanmcdirmid

Rare 1% bugs practically require prints debugging because they are only going to appear only 6 times if you run the test 600 times. So you just run the test 600 times all at once, look at the logs of the 6 failed tests, and fix the bug. You don’t want to run the debugger 600 times in sequence.

lucumo

> the rare, tricky race conditions [...]. The rare ones show up maybe 1% of the time—they demand a debugger,

Interesting. I usually find those harder to debug with a debugger. Debuggers change the timing when stepping through, making the bug disappear. Do you have a cool trick for that? (Or a mundane trick, I'm not picky.)

ozim

It is also much much easier to fix all kinds of all other bugs stepping through code with the debugger.

I am in camp where 1% on the easy side of the curve can be efficiently fixed by print statements.

rtpg

> The rare ones show up maybe 1% of the time

Lucky you lol

What I've found is that as you chew through surface level issues, at one point all that's left is messy and tricky bugs.

Still have a vivid memory of moving a JS frontend to TS and just overnight losing all the "oh shucks" frontend bugs, being left with race conditions and friends.

Not to say you can't do print debugging with that (tracing is fancy print debugging!), but I've found that a project that has a lot of easy-to-debug issues tends to be at a certain level of maturity and as times goes on you start ripping your hair out way more

cik

Absolutely. My current role involves literally chasing down all these integration point issues - and they keep changing! Not everything has the luxury of being built on a stable, well tested base.

I'm having the most fun I've had in ages. It's like being Sherlock Holmes, and construction worker all at once.

Print statements, debuggers, memory analyzers, power meters, tracers, tcpump - everything has a place, and the problem space helps dictate what and when.

kccqzy

The easy-to-debug issues are there because I just wrote some new code, didn't even commit the code, and is right now writing some unit tests for the new code. That's extremely common and print debugging is alright here.

branko_d

Well, if you have a race condition, the debugger is likely to change the timing and alter the race, possibly hiding it altogether. Race conditions is where print is often more useful than the debugger.

mrheosuper

> the debugger is likely to change the timing

And the print will 100% change the timing.

seanmcdirmid

Yes, but often no where as drastic as the debugger. In Android we have huge logs anyways, a few more printf statements aren’t going to hurt.

vodou

The same can be said about prints.

branko_d

Yes, but to a lesser extent.

zarzavat

Even print debugging is easier in a good debugger.

Print debugging in frontend JS/TS is literally just writing the statement "debugger;" and saving the file. JS, unlike supposedly better designed languages, is designed to support hot reloading so often times just saving the file will launch me into the debugger at the line of code in question.

I used to write C++, and setting up print statements, while easier than using LLDB, is still harder than that.

I still use print debugging, but only when the debugger fails me. It's still easier to write a series of console.log()s than to set up logging breakpoints. If only there was an equivalent to "debugger;" that supported log and continue.

jasonjmcghee

Every engineer should understand how to use a debugger and a time profiler (one that gives a call tree). Knowing how to do memory profiling is incredibly valuable too.

So many problems can be solved with these.

And then there's some more specialized tooling depending on what you're doing that can be a huge help.

For SQL, the query planner and index hit/miss / full table scan.

And things like valgrind or similar for cache hit/miss.

Proper observability (spans/ traces) for APIs...

Knowing that the tools exist and how to use them can be the difference between software and great software.

Though system design / architecture is very important as well.

swagmoney1606

Renderdoc!

lock1

So, uh, everything is important, and every engineer must know everything then?

I mean, don't get me wrong, I do agree engineers should at least be aware of the existence of debuggers & profilers and what problems they can solve. It's just that not all the stuff you've said belongs in the "must know" category.

I don't think you'll need valgrind or query planning in web frontend tasks. Knowing them won't hurt though.

h4ch1

I can tell you for a fact a lot of budding web developers don't even know a Javascript debugger exists, let alone something as complex/powerful as Valgrind.

All of these are useful skills in your toolkit that give you a way of reasoning about programs. Sure you can plop console.logs everywhere to figure out control/program flow but when you have a much more powerful tool specifically built for this purpose, wouldn't you, as an engineer, attempt to optimize your troubleshooting process?

nananana9

With native languages you'll almost always be using a compiler that can output debug symbols, and you can use the output of any compiler with (mostly) any debugger you want.

For JS in the browser, there's a often chain of transformations - TypeScript, Babel, template compilation, a bundler, a minifier - and each of these makes the browser debugger work worse -- and it's not that great to begin with, even on plain JS.

Add that to the fact that console.log actually prints objects in a structured form that you can click through and can call functions on them from the console, and you start to see why console.log() is the default choice.

lock1

Yeah, it's quite sad, considering it's already built-in on all major browsers. And it's not even hard to open it, like a click away on devtools tab.

But I think promoting profilers is much more important than debuggers. Far too many people I know are too eager to jump on "optimization" just because some API is too slow without profiling it first.

jasonjmcghee

Understanding how to use these tools properly does not take very long. If you've never used them, spending an afternoon with each on real problems will probably change how you think.

If you don't already know which tool to use / how to diagnose the problem, you'll instead of banging your head against the wall, you'll think - "how do i figure out this thing - what is the right tool for this job"? and then you'll probably find it, and use it, because people are awesome and build incredibly useful free / open source software.

"try stuff until it works" is so common, and the experience needed to understand how to go about solving the problem is within reach.

Like especially with llms, "what's the right tool to use to solve problem x i'm having? this is what's going on. i'm on linux/macos, using python" or w/e

old_bayes

It may sound obvious to folks who already use a debugger, but in my experience a decent chunk of people don't use them because they just don't know about them.

Spread the good word!

gonzo41

This also applies to testing. So much legacy code out there that's untested.

makeitdouble

Depending on the language or setup debuggers can be really crappy. I think people here would just flee away and go find a better fitting stack, but for more pragmatic workers they'll just learn to debug with the other tools (REPL, structured logging, APMs etc.)

giveita

I had a think about where I first learned to use a debugger. The combo of M$ making it easy for .NET and VB6 and working professionally and learning from others was key. Surprised it is less popular. Tests have made it less necessary perhaps BUT debugging a unit test is a killer move. You quickly get to the breakpoint and can tweak the scenario.

NitpickLawyer

> I had a think about where I first learned to use a debugger

Is this not taught anymore? I started on borland C (the blue one, dos interface) and debugging was in the curriculum, 25+ years ago. Then moving to visual studio felt natural with the same concepts, even the same shortcuts mostly.

giveita

Nothing useful I do in my job was taught by another person in a classroom.

nchmy

yeah, tons dont know they exist. But there's also a lot of people - new and veteran - who are just allergic to them, for various reasons.

Setting up a debugger is the very first thing i do when i start working with a new language, and always use it to explore the code on new projects.

giveita

With VSCode it's often a 10 minute job to set up. We are spoiled! Back in the VS days using a Microsoft stack it was just there. Click to add breakpoint then F5.

swaits

Author missed one of the best features: easy access to hardware breakpoints. Breaking on a memory read or write, either a raw address or via a symbol, is one of the most time saving debugging tools I know.

praptak

From the same toolbox: expression watch. Set a watch on the invariant being violated (say "bufpos < buflen") and get a breakpoint the moment it changes.

coderatlarge

windbg used to offer scripting capabilities that teams could use to trigger validation of any number of internal data structures essentially at every breakpoint or watchpoint trigger. it was a tremendous way to detect subtle state corruption. and sharing scripts across teams was also a way to share knowledge of a complex binary that was often not encoded in asserts or other aspects of the codebase.

bpye

This still exists? You can also use JavaScript to script/extend and there is a native code API too.

Note: I do work at MSFT, I have used these capabilities but I’m not on the debugger team.

https://learn.microsoft.com/en-us/windows-hardware/drivers/d...

https://www.timdbg.com/posts/whats-the-target-model/

https://github.com/microsoft/WinDbg-Samples/tree/master

coderatlarge

thanks for the pointers glad to hear it’s all still there

i haven’t seen this type of capability used in too many companies tbh and it seems like a lot of opportunity to improve stability and debugging speed and even code exploration/learning (did i break something ?)

jesse__

Oh my god, same. This literally catches bugs with a smoking gun in their hand in a way that's completely impossible with printf. I'd upvote this 100 times if I could.

yakshaving_jgt

Is there somewhere where this approach is described in more detail?

jesse__

Very roughly, hardware watchpoints are memory addresses you ask the processor to issue an "event" for when they're read from, written to, or executed. This event is processed by the kernel, and passed through to the debugger, which breaks execution of the program on the instruction that issued the read/write/exec.

A concrete use case for this is catching memory corruption. If your program corrupts a known piece of memory, just set a hardware watchpoint on that memory address and BOOM, the debugger breaks execution on exactly the line that's responsible for the corruption. It's a fucking godsend sometimes.

lock1

Search for "watchpoint debugging". Usually in most garbage-collected environments, it just observes & breaks on symbols though, not raw addresses.

Vscode (or an editor with ADP support) supports unconditional breakpoints, watchpoints, and logpoints (observe & log values to the debug console).

m463

debuggers are hard to use outside of userland.

For really hairy bugs in programs that can't be stopped (kernel/drivers/realtime, etc) logging works.

And when it doesn't, like when you can't do I/O or switching of any kind, log non-blocking to a buffer that is dumped elsewhere.

also, related. It is harder than it should be to debug the linux kernel. Just getting a symboled stack trace is ridiculously hard.

makeitdouble

While a debugger is of high value, having access to a REPL also covers the major use cases.

In particular, REPL tools will work on remote session, on pre-production servers etc. _if_ the code base is organized in a somewhat modular way, it can be more pleasant than a debugger at times.

Makes me wonder if the state of debugging improved in PHP land. It was mostly unusable for batch process debugging, or when the server memory wasn't infinite, which is kinda the case most of the time for us mere mortals.

never_inline

I am the author of the posted flamebait. I agree.

I use IPython / JShell REPLs often when the code is not finished and I have to call a random function without entrypoint.

In fact its possible to jump to the graphical debugger from the Python REPL when running locally. PyCharm has this feature natively. In VSCode you can use a simple workaround like this: https://mahesh-hegde.github.io/posts/vscode-ipython-debuggin...

smlavine

It's not a silver bullet, but Visual Studio is leaps and bounds ahead of gdb et. al. for debugging C/C++ code. "Attach to process" and being able to just click a window is so easy when debugging a large Windows app.

jesse__

lol, agree to disagree here. While the interface to gdb is annoying, there are many gui frontend alternatives.

VS, on the other hand, gets worse with every release. It is intolerably slow and buggy at this point. It used to be a fantastic piece of software, and is now a fantastic pile of shit.

squirrellous

IME console-based debuggers work great for single-threaded code without a lot of console output. They don't work that well otherwise. GUI-based debuggers can probably fix both of those issues. I just haven't really tried them as much.

pdb is great for python, though.

MrDarcy

I frequently use the go debugger to debug concurrent go routines. I haven’t found it any different than single threaded debugging.

I simply use conditional break points to break when whatever go routine happens to be working on the struct I care about.

Is there more to the issue?

squirrellous

Thinking back, the issue I had with multi-threaded code was two-fold:

- Things like "continue", "step" are no longer a faithful reproduction of what the program does in real time, so it's more difficult to understand the program's behavior. Some timing-related bugs simplify disappear under a debugger.

- There's usually some background thread that's logging things to console, which reduces to problem 2 in my comment.

I haven't used Go that much. I imagine since goroutines are such a cornerstone of the language, the go debugger must have some nifty features to support multi-(green)-threaded debugging?

sfpotter

It isn't either/or. Good programmers know how to use both and know how to choose the appropriate tool for the job.

hippo22

Most languages let you print the stack, so you can easily see the stack using print debugging.

Anecdotally, dynamic expressions are impossibly slow in the cases I’ve tried them.

As the author mentions, there are also a number of cases where debuggers don’t work. Personally, I’m going to reach for the tool that always works vs. sometimes works.

ksenzee

> I’m going to reach for the tool that always works vs. sometimes works.

This is only logical if you're limited to one tool. Would you never buy a power tool because sometimes the power goes out and a hand tool is your only choice?

nchmy

but can you go back in the stack and inspect the variables and related functions there in print debugging?

truetraveller

This is something that does not require a debugger perse. this is something that can be implemented by a "smart" log. beside the log entry there might be a button to see the trace + state at those points. could even allow log() to have an option for this.

nananana9

But you have to

  1. stop the program
  2. edit it to add the new log
  3. rebuild the program
  4. run it
  5. get the program to the same state to trigger the log
3. can take quite a while on some projects, and 5. can take quite a while too for long-running programs.

And then you see the result of what you printed, figure out you need something else as well, and repeat. Instead you can just trigger a breakpoint and inspect the entire program's state.

MangoToupe

...yes? You just print in the relevant stack frame.

There is an inherent tradeoff between interaction and reproducibility. I think the whole conversation of debugger vs print debugging is dumb. Just do whatever makes you the most productive. Often times it is immediately obvious which makes more sense.

t_mahmood

Maybe someone can give me idea, how can I debug this particular rust app, which is extremely annoying. It's a one of Rustdesk.

It won't run if I compile with debug info. I think it's due to a 3rd party proprietary library. So, to run the app I have to use release profile, with debug info stripped.

So, when I fire up gdb, I can't see any function information or anything, and it has so many system calls it's really difficult to follow through blindly.

So, what is the best way to handle this?

tomjakubowski

You can add debug info to release builds. In Cargo.toml:

    [profile.release]
    debug = true
https://doc.rust-lang.org/cargo/reference/profiles.html#debu...

mrugge

claude code cli

jasonjmcghee

Something I haven't seen discussed here that is another type of debugging that can be very useful is historical / offline debugging.

Kind of a hybrid of logging and standard debugging. "everything" is logged and you can go spelunk.

For example:

https://rr-project.org/