Sorry, this post is coming a day late! Had some family stuff come up and didn't get a chance to finish it yesterday.
Hello again, friends! Let's talk about debuggers.
Squishing bugs and taking names
What is a debugger? Well, usually when we talk about bugs in a computer system we are discussing a few different things. First, there is code that is written incorrectly. We all do this - programming is a constant battle between time and quality. Secondly, there are unintended consequences - maybe an API changes or a vendor removes some code you are relying on. Finally, there are what my company calls immaculate defections - bugs that just appear, seemingly out of nowhere.
A debugger is a program that helps you research and resolve bugs. They can differ, depending on use case - some work to debug compiled programs, some allow access to currently running code, and others debug web systems. Many programming languages include debuggers, which make it easy to start and stop your code at certain points. Logging is an often used method of debugging as well.
What do we debug?
Depending on the language there are a couple of things we might need to debug.
The Stack
This is where many developers spend time with debuggers. The stack is the layers of functions that make up a program. For instance, we might have a function called ReadFile
, which is being called by another function called OpenLog
, which was called by a main
function. In this case, the stack would look like this:
------------
| ReadFile | <- This is the top "frame" of the stack, the function we are currently in
------------
| OpenLog | <- This is the next frame, which is what called the first frame
------------
| main | <- This is the bottom-most frame, which is started all the other calls
------------
Using a debugger;
statement in Javascript will pause the execution of the stack, allowing you to look at a program in a certain state. This can also be down using breakpoints in a debugging program. Similarly, binding.pry
will allow you to do the same thing in Ruby.
Pausing stack execution is the bread and butter of debugging. It allows you to see what is happening inside a function at any given time. This is incredibly useful when trying to track down what could be causing a bug and to check any assumptions you might be making about how a function works.
A stack trace is a textual representation of a stack, after an error has happened.
This is a good demonstration of a Javascript Stack Trace.
Memory
In some languages like C, a developer might need to manage memory. Memory debuggers help you debug things like a memory overflow or improper garbage collection. Most modern lanugages handle this, making memory debugging less useful today.
The Compiled Code
A debugger like gdb
allows you to take a peek inside some compiled code. Many modern debuggers inherit ideas from gdb
, allowing you to set breakpoints in code (even if you don't own it) and walk through the code using next
and continue
statements.
Output
Debugging output is a great way to see what assumptions you might be making about a function or a program. This is most often done by dropping in console log statements and printing out variable values as you run a function. I would say this is 98% of the debugging I do on a daily basis. I really only step into the bigger debugging tools when I am working on a really difficult problem.
Metadata
Finally, sometimes you need to test the "metadata" around a function - things like what it is saving, what it is logging, and what third-party API's it might be accessing. This is a big one in Javascript, as often times you are interacting with browser APIs like localstorage
or service workers. Luckily the browser DevTools help a lot with this. Here is the example of dev.to's localstorage, as viewed through the Firefox Dev Tools:
Fun fact - the first instance of an actual computer bug was by Grace Hopper in 1947. A moth had gotten caught between the relays.
Debugging Scenarios
I don't have a lot more to add to this post, but I want to walk through some common scenarios that I find myself debugging.
Peeking into a functions flow
Debugging programs is all about validating or invalidating the assumptions you are making about said program. Sometimes, those assumptions are about what happens inside a function. Dropping a debugger statement or adding a breakpoint is a great way to stop the execution of a function and walk through it step by step. Pair this with some rubber duck debugging and you'll be well on your way to finding the problem.
Evaluating the process of a third-party library
When you are using a library or a framework, it can help to see how they are using/manipulating the data that you are sending to them. As an example, in React it can be really helpful to debug how often a component is re-rendered.
Tightening the scope
Another really common scenario is using a debugger to find where a problem is not. Often times you may only have 20-30 minutes to spend researching a bug, before you have to do other work. In these scenarios, you can often narrow down the problem set a lot using a debugger. Step through all the code that touches the buggy code and you'll start to get a fuller picture of exactly what is happening. Just make sure you write it down! Never research twice what you could have written down once!
There are so many debuggers out there and a good number have great documentation. In the future, I hope to do a more in-depth post about how to debug (especially in Javascript), but for now hopefully this will help!
What is your experience with debuggers? What kind of questions do you have about deubgging code?
Want to read more like this? I tweet all my posts!
Top comments (0)