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 articleThe 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.
Related
Features I'd like to see in future IDEs
Proposed improvements for IDEs include queryable expressions for debugging, coloration of comments, embedding images, a personal code vault for snippets, and commit masks to prevent accidental code commits.
You've only added two lines – why did that take two days
The article highlights that in software development, the number of lines of code does not reflect effort. Effective bug fixing requires thorough investigation, understanding context, and proper testing to prevent recurring issues.
Thoughts on Debugging
The article emphasizes the importance of reproducing issues in debugging, effective communication, logging for real-time analysis, clear documentation for recognition, and warns junior engineers about the absence of favors in business.
Kernighan's Lever (2012)
Kernighan's lever highlights that while clever coding can complicate debugging, facing challenges fosters skill development, making debugging a valuable learning opportunity that enhances programming capabilities.
Caveman Debugging in the Modern Age
Caveman debugging uses print statements to track code execution. Integrating it with IntelliJ IDEA's Live Template feature enhances productivity through custom templates, streamlining repetitive tasks and improving coding efficiency.
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...
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
}
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.
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.
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.
Really nice to use if you need logs in the terminal
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.
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…
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.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.
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.
Related
Features I'd like to see in future IDEs
Proposed improvements for IDEs include queryable expressions for debugging, coloration of comments, embedding images, a personal code vault for snippets, and commit masks to prevent accidental code commits.
You've only added two lines – why did that take two days
The article highlights that in software development, the number of lines of code does not reflect effort. Effective bug fixing requires thorough investigation, understanding context, and proper testing to prevent recurring issues.
Thoughts on Debugging
The article emphasizes the importance of reproducing issues in debugging, effective communication, logging for real-time analysis, clear documentation for recognition, and warns junior engineers about the absence of favors in business.
Kernighan's Lever (2012)
Kernighan's lever highlights that while clever coding can complicate debugging, facing challenges fosters skill development, making debugging a valuable learning opportunity that enhances programming capabilities.
Caveman Debugging in the Modern Age
Caveman debugging uses print statements to track code execution. Integrating it with IntelliJ IDEA's Live Template feature enhances productivity through custom templates, streamlining repetitive tasks and improving coding efficiency.