Debugging Vs Printing

We’ll admit it. We have access to great debugging tools and, yes, sometimes they are invaluable. But most of the time, we’ll just throw a few print statements in whatever program we’re running to better understand what’s going on inside of it. [Loop Invariant] wants to point out to us that there are things a proper debugger can do that you can’t do with print statements.

So what are these magical things? Well, some of them depend on the debugger, of course. But, in general, debuggers will catch exceptions when they occur. That can be a big help, especially if you have a lot of them and don’t want to write print statements on every one. Semi-related is the fact that when a debugger stops for an exception or even a breakpoint, you can walk the call stack to see the flow of code before you got there.

In fact, some debuggers can back step, although not all of them do that. Another advantage is that you can evaluate expressions on the fly. Even better, you should be able to alter program flow, jumping over some code, for example.

So we get it. There is more to debugging than just crude print statements. Then again, there are plenty of Python libraries to make debug printing nicer (including IceCream). Or write your own debugger. If gdb’s user interface puts you off, there are alternatives.

25 thoughts on “Debugging Vs Printing

  1. All correct as far as it goes.

    Nothing beats good logging for debugging problems that somebody else has with your software. I can’t connect a debugger to somebody else’s computer and minitor it 24/7 until that once in a blue moon error occurs.

    In development and while debugging things that the logging has give me hints about, a good debugger is enormously helpful.

    For getting an approximate location and condition to start searching the code and debugging, nothing beats good logging.

    I am a proponent of “log too much.” If you log it but don’t need to know it, no problem. Filter it out while searching for the cause of the problems. If you don’t log it but need it to find a problem, then you are stuck.

    1. Please include a mechanism to limit log size as well, I’m sure we’re all aware of this, but log too much, and constrain each log’s size based on time or something.

      Logrotate on Linux is wonderful.

      1. OK, so it asked me to log in and then went ahead and posted what I had typed so far.

        There are real-time and multithreaded applications where just using a debugger, setting breakpoints, etc. mess up timing so the program behaves differently, if it runs at all.
        Logging is essential there.

  2. I use IntelliJ and I like the feature of non-suspending breakpoints where I can simply choose to log that the breakpoint was reached, or generate my own message.

    debug prints you can add on the fly!

  3. I don’t use debuggers as much as I used to. There’s something to be said for the simplicity of print statements and logging or, in the case of an embedded microcontroller, wiggling spare I/O pins and looking at them on a scope.

    Debuggers also got me into bad habits: typing in crap code and then looking to see where it went wrong. I’ve found that it saves time to engage my brain and write better code in the first place. Those who live by the debugger die by the debugger.

    I grant that pausing execution, walking the stack and looking at live memory values can be helpful in specific applications, such as reverse engineering malware. However, I’ve been successful at tracking down some pretty nasty stuff–even compiler bugs–without a debugger.

    1. Sometimes the fastest way to an answer is a debugger.

      I’m currently working on a project to swap a no longer supported library with a similar one that does the same job. The difficulty is that the documentation sucks – for the old library and the new one.

      It helps to be able to step through our software using the old library to see what the objects deliver then compare them to what new library delivers so that I can find the needed data.

      Both libraries are for reading an old file format with lots of properties and lots of cruft.

      Our software has to read a file in the old format, extract certain pieces of data, then write it to another file in a different format. The files are both complicated, so we use libraries both for reading and writing.

  4. i don’t think it’s true “that there are things a proper debugger can do that you can’t do with print statements.”

    but man! a lot of things are really onerous with printf debugging.

    i sometimes work on platforms where i don’t have a debugger, or the debugger is buggy. detecting a memory overwrite bug is a lot easier with “watch” but you can do it by printing the value of the memory at every step and watching when it changes. you can view previous stack frames by putting printfs in all the callers. you can even use inline asm to load up the frame base register and actually dump the whole stack. i’ve never met a question i couldn’t answer with printf debugging. just a question of how much time it will take. only time it’s truly painful is when you’re on an embedded platform and you need to don’t have any i/o (or the problem happens before it’s initialized)…doing all your debugging through a single LED or two pins hooked up to the two channels on my oscope…ugh.

    1. What the article doesn’t mention is that you can put the printf in an if statement.

      If you consider the printf as what you would do in the debugger, then the if statement can be wildly complex and detect exactly the error you’re looking for.

      Additionally, there’s also usually some sort of “DebugBreak()” function call, which can be put in an if statement, that will cause the debugger to breakpoint when the conditions are met. Possibly entering a breakpoint when the print is executed, for the best of both worlds.

      I’ve sometimes used multiple prints in a long module, showing “printf(“A”)” at the top, “printf(“B”)” after the 1st step, and so on. With a module containing 10 steps it’s sometimes useful to know which step is causing the memory access area.

      And finally, sometimes you just don’t have access to a debugger, but almost every program has access to some sort of I/O that can be used to show status. The printf really shines here.

  5. Printf is just so much more accessible compared to an actual debugger, but anything serious (memory leaks, race conditions, etc) are all better with GDB. I highly recommend using GDB with vscode. The UI makes it very easy to set breakpoints, view variables etc

    In python, you can actually drop into interactive mode when an exception occurs so that you can play around with variables and objects in real time and figure out what’s wrong. Super helpful!

  6. I agree; debuggers are very cool. I love them so much when they work.

    Now if only I could configure PlatformIO + GDB to

    Break at the actual breakpoint, rather than three lines before or ahead (no idea what’s going on there)
    Actually skip stdlib and 3rd-party lib code on the way to code that I care about on the native platform, like I asked it to
    Reliably unwind the stack and give me some meaningful output and, I dunno, pause for a bit before FreeRTOS reboots after a gnarly uncatchable error

    Then I could actually use it!

    1. Break at the actual breakpoint, rather than three lines before or ahead

      Are you building with optimization enabled? This tends to produce such results, because the machine code is no longer a linear translation of your source. Also causes single-stepping to jump around wildly instead of advancing one line after the other.

      Of course, disabling optimizations changes timing and can either make it impossible to run successfully at all, or mask the bug. So sometimes there’s no way around debugging optimized builds. If in doubt, look at the assembly and don’t let the IDE fool you.

  7. I have to agree. Good tools save a lot of time and hassle. I still use print statements mainly when I am trying to integrate a new device or sensor bit and things go wrong with the library the first shot. You can kinda tell which errors are going to be a multi hour head banger vs an errant semi colon. I still use commenting too to take things in and out of the code as I rule out the angry bit. As I always try to tell people though, use the thing that works for you. It just depends on the mission at hand :)

  8. Professional software developer here. I would like to add that there is a third way which is often FASTER than debugging in some contexts: TDD (Test Driven Development).

    Iterating complex business logic by following a strict red-green-refactor cycle negates the need for a debugger. If unit tests are not coupled to the implementation, they can also prevent future regressions and even help understand (document) the application behavior.

    1. By debugging on production you can force your developers to get better, fast!

      Debug, testing, staging, unit testing, source control, bug tracking, debuggers?
      You are setting low expectations, no wonder you get bugs.

      If you have a good crew, you can ship anything that compiles and links.

      1. Yeah. It’s also fun to write code on the plane ride to a client site and roll it into production upon arrival, pretending like it’s stable or even tested. Absolutely no pressure!

        Yeah, that was a job for 20-something me.

      2. “If you have a good crew, you can ship anything that compiles and links.”

        ( I’m assuming sarcasm font on that :P )

        As an ex-SysOp; The good devs in my team, and boy I worked with so many brilliant people, knew it so did try to get their latest compilation through the test rigs and proper QA/UAT processes. But sometimes silly sales promises did unfortunately force a (fr)agile “if it compiles, ship it!” situation.

        Logs and ‘–debug’ always had their place as the first thing to check and ~90% of the time were enough for me, but GDB was still an essential tool. When those mission-critical international systems abend’ed at 3am on some unexpected garbage and wouldn’t come back up cleanly; GDB usually quickly exposed the errant data, which I could then seek permission to correct/purge.

        I strongly feel, from many decades of experience, a good dev should absolutely put a pile of prints behind a –debug switch, especially in those ;TODO sections that will never cough be triggered.

        Some SLAs are not very forgiving.

  9. Another Professional Software Developer here,

    I’ll emphatically proclaim that having a good debugger (and environment) is worth every effort in getting one setup!!

    There will always come some scenario where we are ask ourselves the question of: ‘why is this not working the way it should’. Having the ability to step thru the code, view each variable, and watch the process misbehave gives more valuable insights and is faster than the cycle of guessing where to add a ‘print’, [compile,] and re-run.

    I’ve worked a lot in the ‘old-world’ languages of: C++, Perl, php, Java. I’m now diving into the newer languages of Python, TypeScript (aka JavaScript), Scala, and even Apache Spark. Every one of these I’ve worked with have a debugger, and for good reasons. When the process results or behavior are unexpected, the root problem is not usually obvious with simple ‘print’s. This can also apply to TDD, when your tests fail for unexpected reasons.

    If you don’t already know them, these are some of your friends:
    * python -pdm
    * perl -d
    * node -inspect (for TypeScript / JavaScript)
    * gdb (for C/C++/Fortran/etc…)
    * For Java & Scala there’s is the JVM option of “-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=XXXX” JVM option

    Additionally
    * valgrind (for C/C++/Fortran/etc…) , helps with finding memory leaks, and thread race conditions, and profiling of compiled (ELF format) binaries. Valgrind also works with Python.

    And lastly there’s an entire subject of profiling your process. Again, every one of these languages have a profiler. Know thy code!

  10. I only use a debugger for debugging algorithms. Its a useful tool but even the best tools interfere slightly with execution timings which can have disastrous results in a system, especially one with multiple processors. All tools are useful, just avoid the temptation to just throw code together and ‘debug on the fly’ — especially with MCU type projects the problem’s rarely in the actual code.

    1. Besides that, there’s also defmt for when you don’t want to use the debugger. It’s pretty lightweight and you only need some single-direction stream to get the messages from the mcu to your pc.

Leave a Reply

Please be kind and respectful to help make the comments section excellent. (Comment Policy)

This site uses Akismet to reduce spam. Learn how your comment data is processed.