October 6th, 2024

Local Variables as Accidental Breadcrumbs for Faster Debugging

The blog post highlights the significance of local variables in debugging, emphasizing their role in providing context for errors, and suggests improvements for error tracking tools like Bugsink.

Read original articleLink Icon
Local Variables as Accidental Breadcrumbs for Faster Debugging

The blog post by Klaas van Schelven discusses the importance of local variables in debugging, particularly in the context of using Bugsink, an error tracking tool. It emphasizes that local variables serve as "accidental breadcrumbs" that provide crucial context when exceptions occur in code. The author explains that exceptions often arise not at the point of error but in subsequent function calls, making it essential to have visibility into local variables at the time of the error. The post contrasts two coding styles: one that uses explicit local variables and another that relies on inlining or object-oriented approaches. The former allows for better error tracking since the values of local variables are captured in stack traces, while the latter can obscure this information, complicating the debugging process. The author suggests that while tools like Bugsink are effective, they could be improved by capturing more context, such as object attributes. Additionally, the post advocates for using assertions to clarify assumptions in code, which can help in identifying errors closer to their source. Ultimately, the author encourages developers to consider the implications of their coding style on debugging efficiency and code maintainability.

- Local variables are crucial for effective debugging and provide context in stack traces.

- Inlined or object-oriented code can obscure important variable information during errors.

- Tools like Bugsink could benefit from capturing more contextual information, such as object attributes.

- Using assertions can help clarify assumptions and improve error identification.

- A coding style that favors simplicity and explicit local variables enhances readability and maintainability.

Link Icon 17 comments
By @lapcat - 6 months
> Should you really change your coding style just for better debugging? My personal answer is: not just for that, but it’s one thing to keep in the back of your mind.

My personal answer is yes, absolutely.

15 years ago I wrote a blog post "Local variables are free": https://lapcatsoftware.com/blog/2009/12/19/local-variables-a...

Updated 7 years ago for Swift: https://lapcatsoftware.com/articles/local-variables-are-stil...

By @jph - 6 months
> add assertions to your code.

Yes, and many programming languages have assertions such as "assert greater than or equal to".

For example with Rust and the Assertables crate:

    fn calculate_something() -> f64 {
        let big = get_big_number();
        let small = get_small_number();
        assert_ge!(big, small); // >= or panic with message
        (big - small).sqrt
    }
It turns out it's even better if your code has good error handling, such as a runtime assert macro that can return a result that is NaN (not a number) or a "maybe" result that is either Ok or Err.

For example with Rust and the Assertables crate:

    fn calculate_something() -> Result(f64, String) {
        let big = get_big_number()
        let small = get_small_number()
        assert_ge!(big, small)?; // >= or return Err(message)
        (big - small).sqrt
    }
By @jmull - 6 months
I'm a fan of this.

Not just for debugging either. Giving something a name gets you to think about what a good name would be, which gets you thinking about the nature of the thing, which clarifies your thinking about the thing, and leads you to better code.

When I've struggled to figure out what the right name for something is, I sometimes realize it's hard because the thing doesn't really make sense. E.g., I might find I want to name two different things the same, which leads me to understand I was confused about the abstractions I was juggling.

But it's also always nice to have a place to drop a break point or to automatically see relevant values in debuggers and other tools.

By @rqtwteye - 6 months
I have done this since a long time. I always thought I am too dumb to read and debug complex code with multiple function calls in one line. I always put intermediate results into variables. Makes debugging so much easier.
By @drewg123 - 6 months
The problem I always have with locals (in kernel code written in C) is that the compiler tends to optimize them away, and gdb can't find them. So I end up having to read the assembly and try to figure out where values in various registers came from.
By @tetha - 6 months
This is kind of related to a change I recently made to how I structure variables in ansible. Part of that is because doing even mildly interesting transformations in ansible, filters and jinja is just as fun as sorting dirty needles, glass shards and rusty nails by hand, but what are you gonna do.

But I've started to group variables into two groups: Things users aka my fellow admins are supposed to configure, and intermediate calculation steps.

Things the user has to pass to use the thing should be a question, or they should be something the user kind of has around at the moment. So I now have an input variable called "does_dc_use_dhcp". The user can answer this concrete question, or recognize if the answer is wrong. Or similarly, godot and other frameworks offer a Vector.bounce(normal) and if you poke around, you find a Collision.normal and it's just matching shapes - normal probably goes into normal?

And on the other hand, I kinda try to decompose more complex calculations into intermediate expressions which should be "obviously correct" as much as possible. Like, 'has_several_network_facing_interfaces: "{{ network_facing_interfaces | length > 0 }}"'. Or something like 'can_use_dhcp_dns: "{{ dc_has_dhcp and dhcp_pushes_right_dns_servers }}'.

We also had something like 'network_facing_interfaces: "{{ ansible_interfaces | rejectattr(name='lo') }}"'. This was correct on a lot of systems. Until it ran into a system running docker. But it was easy to debug because a colleague quickly wondered why docker0 or any of the veth-* interfaces were flagged as network-facing, which they aren't?

It does take work to get it to this kind of quality, but it is very nice to get there.

By @Freedom2 - 6 months
> accidental

Besides a debugger, isn't one of the first things people do (even undergrads) is start logging out variables that may be suspect in the issue? If you have potentially a problematic computation, put it in a variable and log it out - track it and put metrics against it, if necessary. I'm not entirely sure a full article is worth it here.

By @maleldil - 6 months
Interesting how this seems to go completely against another post I saw here: https://steveklabnik.com/writing/against-names/
By @animal_spirits - 6 months
The Rich package has trace back support that inspects local variables for every stack in the trace: https://rich.readthedocs.io/en/stable/traceback.html

Really nice to use if you need logs in the terminal

By @ajuc - 6 months
This is my main problem with introducing functional programming in OOP languages (like streams in Java).

If it was a for loop I'd know at first glance at the exception what exactly failed...

If your language & IDE does not support functional programming properly with debugger and exception reporting - don't do it.

By @forrestthewoods - 6 months
I bloody hate Python stacktraces because they usually don’t have enough information to fix the bug. The curse of dynamic languages.

What’s the easiest possible way to cause stacktraces to also dump local variable information? I feel like this is a feature that should be built into the language…

By @mkehrt - 6 months
I strongly believe nested expressions increase cognitive overhead. Between the two examples in the blog post

  def calculate_something():
     big_number = get_big_number()
     small_number = get_small_number()
     return math.sqrt(big_number - small_number)
vs

  def calculate_something():
     return math.sqrt(get_big_number() - get_small_number())
I'll pick the first one every time. This is a bit of an extreme example, but our languages provide us with the ability to extract and name subexpressions, and we should do that, rather than forcing people to parse expression trees mentally when reading code.
By @rafaelbco - 6 months
In Zope you can create a local variable named __traceback_info__ and its value will be inserted in the traceback. It is very useful.

Like add a line to a log, but only when an traceback is shown.

See: https://zopeexceptions.readthedocs.io/en/latest/narr.html#tr...

Seems like the zope.exceptions package can be used independent from Zope.

By @Terr_ - 6 months
> Accidental

What? For whom? I've been extremely intentionally breaking up longer expressions into separate lines with local variables for a long time.

Writing local variables as "breadcrumbs" to trace what happens is one of the very first things new developers are taught to do, along with a print statement. I'd wager using a "just to break things up" local variable is about as common as using them to avoid recomputing an expression.

... Perhaps the author started out with something in the style of Haskell or Elm, and casual/gratuitous use of named local variables is new from that perspective?

> However, the local variables are a different kind of breadcrumbs. They’re not explicitly set by the developer, but they are there anyway.

While I may not have manually designated each onto a "capture by a third-party addon called Bugsink" whitelist, each one is very explicitly "set" when I'm deciding on their names and assigning values to them.

By @jayd16 - 6 months
Without giving an opinion on coding style, it seems like IDEs/debuggers could present the unnamed values this way.
By @robdar - 6 months
I suggested this on a thread in /r/cpp a few years ago, and was downvoted heavily, and chewed out for the reason that coding for ease of debugging was apparently akin to baby killing.