Learning syntax is only a small part of being a developer. The transition from coding bootcamp or online tutorials to the Real World can be a struggle. The objective of this series of blog posts is to summarize the knowledge that I gained during my own transition period (and beyond), and pay it forward to those that find themselves in a similar position.
A reminder of the debugging topics that we will be covering:
Bugs and errors are normal parts of software development, because developers are only human. We have already covered some tools that will help us debug our program. In this post, we will have a look at some approaches that have worked for me. They're not "best practices" (what does that mean, anyway?!), just some mindsets I have picked up from my mentors and peers along the way.
Recently, my pair Shamyle and I wrote a method to sort some ActiveRecord objects returned from a couple of queries into one list. We had one database query that returned an ActiveRecord Relation, a list of Evaluations, and another query that returned one single Evaluation. The purpose of our method was to sort these results into one list based on certain attributes, so that the frontend will display them in the proper order.
Something like this (shortened for reasons):
def display_evaluations list_query = Evaluations.where(...attributes) single_query = Evaluations.find_by(...other attributes) list_query << single_query list_query.sort(date: :desc) end
With a fully green test suite and the UI revealing no unexpected behaviors, after the team reviewed it, we merged it into the codebase.
A couple hours later, when Sasha was working in the QA environment, she found that the page we worked on was broken. After a little bit of investigating, she concluded that the page couldn't render because one of those queries to the database returned nothing. Our method was adding a
nil into the list of objects we pass to our frontend, but when it tried to display something that was undefined, everything fell apart.
One of the first things I do when I find a bug now is write tests because we want to make sure that any future changes to the code won't result in the same bugs again. Tests to cover the conditions under which our bugs show up will prevent that code regression and let us know when the bug has been fixed.
For the case above, I ended up working with her to find a solution for the bug and the first thing she suggested we do was write tests to cover this particular state of the program. Our test basically said, "hey, if one of the queries came back with no result, exclude it from the list of objects to render by not pushing it to the array we return!"
We didn't know when we started debugging what specific lines of code we needed to change or what that change looked like. But backed up by the tests, we would know right away when it's been fixed. Having comprehensive tests can also help with our next tip.
When I first started coding, I had the tendency to make (what I know now to be) wild guesses about the cause of my bugs. I would encounter something that's broken and immediately try to fix it in the code, glossing over the error messages. In my random attempt at a solution, I might touch multiple files and methods. At the end of the flailing, the bug would either still be there or something else altogether was broken, and I would have no idea where to begin backtracking the many changes I just made. 😱
Tech folks like to talk about TDD (Test Driven Development) as a Best Practice. I would also define TDD as Test Driven Debugging. Regardless whether our task is to create a new feature or fix a buggy old feature, having tests gives us a roadmap to our goal.
Sasha and I wrote our tests, and much like with the TDDev approach, each change in code only addressed the specific way the tests were failing or a specific error we saw at that moment. So each time we tried a solution, we would run the tests, hoping that they would fail in new and exciting ways, informing our next step(s).
Bugs exist because we think our codes works one way when it really works another way. There are things about our program that we take for granted to be true. How many times have you said something like this:
This method returns an array of objects, so when we call it here, the other method can iterate through it.
...only to find that the first method returns a different data structure sometimes?
They say that the first step to recovery is admitting that you have a problem. In order to debug efficiently, we have to admit that what we think we know about our code isn't always true. We can begin by asking ourselves these questions:
🚨What do I think my code is doing?
🚨What is my code actually doing? (by using some tools from Parts 1 and 2, maybe)
🚨How does this disconnect cause problems in the program?
In the case of our example:
💡I thought both of my queries would return something.
💡...but in actuality, those queries could return nothing from the database.
💡When the frontend tries to iterate through and display components for each item in the list, it doesn't know what to do with an
Sometimes in our jobs as developers, it feels likes we're just putting out fire after fire.
So, good debugging habits are almost as important as writing clean code.
In a perfect world, bootcamps would put more emphasis on teaching the techniques and practice of debugging; it would prevent their graduates from learning bad habits and prepare us better for our first jobs.
I'd like to go back to Aaron Patterson again here to close this talk. I found a tweet the other day, that he had pinned from back in January.
Knowing Tenderlove, he was probably making a joke, but this actually made me feel more at ease about debugging. Because when we frame it in this way, errors and bugs are just edge cases that we haven't considered yet. So we shouldn't be intimidated by them.
And, I hope after these three posts, you are now empowered to debug with confidence!