Published on

Overdebugging

Authors

Changes

Change, change never changes.

Imagine there was a new feature request and turns out that some of it needs to be "added" to existing parts.

Changing the existing code.

What feelings would you have?

Excitement?

Fear?

Joy?

We are professionals so it's not daunting to work with such an event.

Requirements are pretty straightforward so it should be a breeze to find a proper solution.

How does it work?

Depending on the habits, experience and approach, various people start from different positions.

Some are sketching (e.g. using excalidraw) to perform more visual thinking.

Others are just rushing and jumping into the code and hack things around to test out some concepts and ideas.

Eventually, everything starts and ends in the code itself.

Let's pretend, as we are changing existing code, that there is a massive legacy method (or a function).

Hundreds of lines.

Of course, there are some private methods (or non-exported functions), so there is some decomposition.

As it was long time that we've touched this code, we need to build mental representation of this code.

What could we do?

Probably, the simplest thing is to just run the system - whether it is an API endpoint, SPA application's specific view or similar.

Turns out, in order to understand it better, we placed a bunch of log statements and few debugging breakpoints.

Quite familiar, isn't it?

Not working?

After understanding how the system works at the current moment, we rushed to put some new code here and there.

Maybe we extracted some more private methods (or non-exported functions) and made the code a little bit "cleaner" (subjectively, of course).

Running the system again and boom - doesn't work as we would expect it to.

Simple - let's add more logging and breakpoints to see where did we fail with transferring our understanding.

Couple of more loops with such breakpoint/log steps, interleaved with running the system, and we are almost finished.

Finally, expectactions are met.

What problem are we trying to solve?

Whether we are running the system to understand how does it work or to check if the increments works as we want to - we keep expectactions in our mind.

We can put log statements, breakpoints, or other "inspection" tools, with a specific purpose - to check expected behavior.

So expecations are implicitly kept in our heads, whereas "inspection" instruments will provide the feedback about the functional state of the given part of the system.

Without neglecting the simplicitly of such approach, and pretty fast feedback coming from utilizing it, it has its flaws.

Log statement just informs us about inner state and we are then judging if it matches our expectations or not.

"It's just a log", one might say.

In Essentially bounded, Accidentally unlimited, I concluded that:

Conclusion 🔍

There is no single solution, but there is a single problem.

So actually, what problem do we want solve to right now? (if you are interested where does this question come from, please check Software Engineering for busy parents)

whether the system is doing what we expect.

In other words, whether the system is satisfying our expectations, we specified in our mind through some mental representations.

Expectations?

One possible solution is to use debugging approach by placing log statements or breakpoints.

Run the system, let the debugger hit the breakpoint, we are at home. How many levels did we cross? Doesn't matter, the problem is solved (of inspecting the system's state).

For now.

We can inspect the state of the system to confirm or to falsify expectations.

This is probably the most straightforward approach.

What could be an alternative solution?

There are some key concepts we can enumerate: expectactions (or requirements), specification of them, feedback.

Does it ring a bell?

Question 🤔

What problem are we trying to solve right now?

As you probably guessed, I am thinking about specifying expectations, of how the (sub)system should work, through tests (you might wonder why I am so expressive - if you are curious, please check "The ambiguity" of TDD).

This is another way of how we can get the feedback from the system (or its part(s)).

How fast can you write a specification?

"But it might be slower than just putting a bunch of breakpoints or log statements.", one might say.

Creating a test file, thinking of proper cases and so on. It is an initial effort, an investment, one needs to do.

What if writing tests, for a particular part of the (sub)system, is difficult? Should we really blame tests for that?

Obstacles, obstacles everywhere.

As we are professionals, and we don't like to waste time (do we?), it's quicker to put those log statements.

It doesn't require that much of the mental energy.

But aren't we lying to ourselves?

As my friend, Przemek Wolnik, says: "head isn't the best place to keep ideas", we could use similar reasoning and say:

Conclusion 🔍

Head isn't the best place to keep expecations.

Soon, mentioned mental energy would be required to load the state of the system into our head, follow each line, understand bits and pieces.

So we are not getting read of thinking about the scenarios in which our system currently might be, we are just deferring it.

What's more, we are not leaving any trace for others (including future ourselves), what was expected behavior.

Overdebugging

Placing logs here and there also doesn't encourage decomposing our system into smaller, manageable parts.

It might be a band-aid for a broken arm - if our methods/classes/functions are large.

Breakpoints might be even much more "effortless".

Yet again, they don't give feedback about the structure of our system.

It's easy to add breakpoint or log statement to get feedback from runtime state, but we won't get any feedback on the architecture, structure, and coupling (and cohesion too).

Tests could tell us so (if we are actually listening to them!).

Sometimes I feel we are addicted to debugging (either through breakpoints or logging). Almost as if we were tempted to try out the narcotic called "debugger".

It gives the dopamine rush, an immediate reward, leaving with an unspoken feeling of emptiness.

How to break out of this viscious circle?

Don't keep specifications in your head

Firstly, you need to admit you are addicted.

Don't be shy, tell it aloud: "I am addicted to debugging".

Secondly, try to specify really simple, happy path cases, in the form of a plain text list, what do you expect from a part of your system, that you are currently working with.

Finally, start with the first item on the list and try to write a test for it.

It might not be that easy. Don't get discouraged.

This is a signal, a feedback, a message from the friendly world (or test?) about your system - use it wisely!

  • Does it take a lot of the time to set up and run the first test? Then maybe you need to work on making this processe easier and faster.
  • You can't test a given piece of functionality? Then maybe you need to extract a smaller function/class and test just it.
  • Your code relies on external services (e.g. browser localstorage or 3rd party service)? Then maybe you are missing proper abstraction.

There is no finite set of answers on where and how to start, but it boils down to finding the simplest possible case and not being afraid of writing the "ugly" code to satisfy it.

No debugging, more testing

Next time, when you will feel the urge to run the entire (sub)system, place breakpoints or a bunch of log statements to inspect the state - slow down.

Take your time for explicitly specifying what are you expecting to see, e.g. in the log statements, and try to write your first specification through a test.

If it's difficult and you feel miserable - you can be grateful and say "thank you" to this little hero - to a test.

It is your ally in design and development. If you treat it well and ask for help, soon you will be rich in information about the state of your system.

You can even use a specific discipline and restrict yourself from debugging and logging - only specifications.

Sooner or later, it (specification-driven development) will become a habit.

Of course, you can ask, dear Reader, "should I always use this approach?".

My answer is stable - yes, my zealot, and now - give me fifty.

Treat it as a play or an exercise. The only thing you can lose are some ego points when you'll be struggling.