A note before we begin: One of my favorite things about programming is debugging. I recognize that this statement may not resonate with everyone in the larger software engineering community, especially anyone who is currently working on a particularly tricky bug, but all the same I enjoy the process. To me, it provides the thrill and satisfaction normally limited to the kinds of mysteries found in books and movies. Real-life mysteries are messy and often go unresolved. Their counterparts from narrative storytelling always provide just enough breadcrumbs to allow the characters to reach a conclusive, satisfying ending. Solving the riddle of a buggy piece of code provides the best of both worlds. It is a real-life problem whose solution provides a path forward and a reason to celebrate, yet at the same time you can count on the existence of some sort of culprit embedded in your code, along with all the clues you need to track it down - after all, you're the one who put it there. So in the spirit of savoring these mysteries (because the alternative is letting them eternally frustrate you and I greatly prefer door number one) I will from time to time describe a debugging journey that I found particularly interesting or useful. Enjoy!
As part of my studies at the Flatiron School, we were recently tasked with re-creating several methods that operate on arrays and objects. Picture workhorse functions such as map(), filter(), and my focus for this particular post, array.length. The overall lesson focused on functional programming and we were tasked with duplicating these processes without just using the original methods. The name for my faux-function was stipulated to be mySize(). I came up with the following process to provide the intended behavior:
Initialize a counter variable
Create a copy of the original array which was passed as an argument
Loop through the copied array and for each element, increment the counter
Return the counter
All told it was a simple function. Most of my work had to do with designing the loop that iterated through the array. One way to do this is with a simple for loop:
The problem here is that in order to tell the for loop when to terminate, I needed to use the array.length property, which is the very function I am attempting to mimic with mySize. I could have used the (for...of) syntax provided by JavaScript as a way to avoid this, but I wanted my code to show the inner workings of finding the length of an array, and I felt like using the syntactic sugar of a ready-made array iterator just abstracted away the same problem faced by the other for loop.
I decided to use a while loop which incremented the counter while removing an element from the array each time through. Once there are no more elements in the array, the condition of the loop returns false and we're all set:
Once I added the other parts of the function I ended up with the following:
Astute observers of code may already see my error, but stick with me, as my journey to enlightenment took me to some fascinating places, both technically and emotionally.
Upon running the provided tests on the code I received the following info about the test I failed:
I took a look at the test itself for more information:
Comparing these two bits of code I got my first whiff of something unusual. The test used an array with four elements: [1,2,3,4]. It ran this array through my function, getting the integer 4 as the answer for its length. It compared that value to the return value of array.length for the same test array. That method returned a value of zero. I had suddenly found myself in some sort of Bizzaro World where my function returned the correct answer and the test was telling me [1,2,3,4] had a length of zero.
Quick public service aside for all my fellow new developers out there: if you start to suspect that your code is just fine and there's an error with the test suite provided, or perhaps there's a bug in the compiler preventing your code from compiling: stop. Ninety-nine percent of the time it's you, as a conservative estimate. Telling yourself otherwise only sends you in an unhelpful direction when you should be focusing on what you did wrong.
I resisted the urge to blame this on the tests, and tried a few other diagnostics in order to see what my function was producing:
So far so good. Next, I decided to use an equality to compare mySize() to array.length just like in the test that failed:
Just like in the test, these two values were not equal. On a whim, I decided to reverse the order in the equality:
Now we're getting somewhere. When I call the built-in method first and my own function second, they both return the same result. This subtle change was the clue I needed to break this case wide open. It suggested that my function was mutating the original array when run. One more quick trip to the console confirmed my suspicions:
Every time I used the array.pop() method on what I thought was the copy of the array passed to mySize(), I was actually removing an element from the original array. Ironically enough, during a lesson on functional programming, which relies on pure functions that avoid mutating data and side effects outside of their own scopes, I created a function that does just the opposite, spilling its messy contents onto any test or context it is called in.
My rookie mistake: assuming that the assignment operator made a copy of the original array. All I did was create another pointer to the same array. Luckily there is a quick fix. Adding .splice() onto the end of the array I wanted to copy guaranteed that a shallow copy of the array is created. This copy can be emptied safely without mutating the original. The following function passed all the tests.
The necessary change can seem like a small detail, but it was one that sent me down an interesting rabbit hole of testing and debugging. It taught me a valuable lesson about just how far away from your original code your mistakes can spread if you're not careful about the data that your functions are operating on.