Skip to content

Instantly share code, notes, and snippets.

@sidk
Last active March 24, 2019 17:17
Show Gist options
  • Save sidk/5b9fb4c1776473376e1fc3c194ef3b28 to your computer and use it in GitHub Desktop.
Save sidk/5b9fb4c1776473376e1fc3c194ef3b28 to your computer and use it in GitHub Desktop.

My go-to debugging methodology for logical errors

Assumptions:

  1. You've identified one function that's not doing what you want. Though we'll briefly look at how to narrow down a broader problem (like a bug report) to one function at the end of the article, that won't be the focus of this article.
  2. Your function does not have any side-effects. In other words, it does not have any effect on the world outside itself. While you can definitely apply the methodology discussed here to functions with side-effects, we won't talk specifics here.
  3. You have a reliable and easy way to execute your function and inspect its return values for a wide range of inputs. This can range from a simple console.log statement all the way to a full fledged test suite.
  4. You don't have any syntax errors in your code.

Debugging process for a logical error:

Assumption: Logical error here is defined as: Your function returns the wrong thing, either always, or when given a specific set of inputs.

Step 1: Make a mental model in your head for what you expect the function to return.

A good way to start this process is to ask yourself "What type of data does my function return?". The answer to this is typically one of the various types that are part of the Javascript language (number, string, Object, Array etc).

Once you have this in mind, start drilling down into the specifics of this data. For example, if your function returns an object, ask yourself what the object should look like. What are its keys? What should their values be? How do the keys and values change if the input to function changes? This model doesn't have to be super in-depth, so don't worry if you don't get too far in drilling down into the specifics. While it is true that the more in-depth you get, the easier the next steps will be, this step primarily exists to get you in the right frame of mind for following the next steps.

Step 2: Take note of your function's current erroneous ouput and how it differs from the mental model you made in step 1.

Step 3: Identify a spot in your function you can investigate

This spot should be:

  1. At least one line before the return statement, and
  2. Influences the return value

In subsequent steps, you will investigate this spot to gain insight into why your function is not working.

If you need some inspiration, consider:

  1. A variable that your return statement uses to construct the return value
  2. An intermediary function result, if you're calling another function
  3. The inputs sent into your function.
  4. The return statement itself, if it is anything more complicated than returning a variable

The exact spot doesn't matter as much as picking one to start with. Over time, your intuition will grow. Let's go through an example to illustrate. The function below ought to return an array containing the divisors of a non-prime integer, but for some reason, it doesn't:

const getDivisors = integer => {
 const arrayFrom1ToN = Array.from(integer).fill(0).map((_, i) => i + 1)
 const divisors = arrayFrom1ToN.filter(n => integer % n)
 return divisors.length ? divisors : `${integer} is prime`
}

We'd notice with this function that if we sent in an input of 12, we'd get a return value of [5, 7, 8, 9, 10, 11]. Since what we want is a return value of [2, 3, 4, 6], one potential point that is at least one step before the return statement and also influences the return value is where we create the divisors value with the const divisors = ... declaration. There could be something going on there 🤔, so let's investigate that!

Step 4: Insert a console.log at that spot

Is the printed value what you expect? If it is what you expect, then go back to step 3 and identify another point where you could insert a console.log. If it's not, excellent! You've come one step closer to fixing your function! Now, its time to guess what could be causing this value to be incorrect. Once you've guessed at a couple of potential causes, you will repeat this step by inserting console.log to either confirm or deny your guess, and keep going until you've found the cause.

For the example above, we'd insert a console.log(divisors) right after we've declared and calculated it. At that point we'd notice that the value that is printed is exactly equal to our erroneous return value. This gives us two things to investigate, since it tells us either that:

  1. The error lies in the function that's passed into filter (n => integer % n), or
  2. There's an error in the line above it (const arrayFrom1ToN = ...)

Let's look into the filter function. We're back to step 1 at this point, since we have a function that might not be working correctly. Here's how I'd step through debugging the filter function:

  1. Prepare a mental model: I want this function to return true or a truthy value when integer is divisible by n. This will ensure that any number that integer is not divisible by will not make it into the divisors array.
  2. We take note that the array that is returned by this filter contains only numbers that are not divisible by the input integer.
  3. We now pick a spot that is one line before the return statement. In this particular case, our filter function has only one line, so we must re-arrange things such that we can actually pick a spot. Let's try this:
const getDivisors = integer => {
  ...
  const divisors = arrayFrom1ToN.filter(n => {
    const returnValue = integer % n
    return returnValue
  })
  ...
}

This particular trick of "extracting" out the return value into a const is one you can use in many contexts. Now we have a place to insert our console.log!

  1. Insert the console.log right after the const returnValue = ... statement. In this particular case, I actually want to know what n is for each corresponding returnValue, so I do something like:
const getDivisors = integer => {
  ...
  const divisors = arrayFrom1ToN.filter(n => {
    const returnValue = integer % n
    console.log("n: ", n, "returnValue: ", returnValue)
    return returnValue
  })
  ...
}

After inserting this statement, I'd run getDivisors to see what my console.log prints out, and see something like (assuming integer is 12):

n: 1 returnValue: 0
n: 2 returnValue: 0
n: 3 returnValue: 0
n: 4 returnValue: 0
n: 5 returnValue: 2
n: 6 returnValue: 0
n: 7 returnValue: 5
n: 8 returnValue: 4
n: 9 returnValue: 3
n: 10 returnValue: 2
n: 11 returnValue: 1
n: 12 returnValue: 0

Our mental model tells us that the function should return true or a truthy value (something that is not 0 in this case) whenever integer is divisible by n. The function is doing the opposite, since we can see that it returns a falsy value (0) for all n that are divisors of 12. This is the likely cause of our function not working.

Step 5: Brainstorm a fix.

This is the most creative part of this process and is where you get to exercise your coding skills. Continuing with the example above, since we want our filter function to return true when integer % n is 0, we make the following modification to it:

const getDivisors = integer => {
  ...
  const divisors = arrayFrom1ToN.filter(n => {
    const returnValue = integer % n === 0
    return returnValue
  })
  ...
}

🎊 And that fixes it! 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment