November 15, 2015

IDR 06: NullPointerExceptions and sleepless nights

Since Friday afternoon, I've been chasing a bug in some old code. The bug which I mentioned in my previous post that I had finally managed to fix was indeed fixed; but I wasn't using the new code correctly, and that led to a new one.

This bug was one that was ultimately pretty simple to fix: something called a NullPointerException.

In programming languages, we have variables. These aren't too unlike variables from Algebra problems - we give the variable a name and use it in the place of whatever actual value it will be set to when our code is eventually called. So we can write a block of code like this (in Java):

public int add(int a, int b) {
  return a+b;
}
We have at least two additional concerns when programming that aren't immediately obvious from Algebra:
  • The variables have a type (int in the snippet above)
  • The variables have an address
[ As an aside, variables in Algebra do usually some kind of "type": Rational, Irrational, Integer, etc. This is somewhat analogous to types in programming languages, in that the type implies some characteristics of a variables value.]

In Java, the type of a variable is meant to be obvious. You have to specify the type everywhere, you can't "mix" types unless you add some additional overhead, etc. The idea is that the programming language will help you enforce this type so that you're less likely to make type-related bugs. It also makes the language more efficient at optimizing your programs.

The address of variables is more interesting, though. If we wanted to add two numbers together out here in real life, we don't need to worry about addresses. Just tell me what numbers you'd like to add (ie, the values of those numbers, like 2 and 5), and we can add them.

For "simple" values, this might be OK, but the types in modern programming languages can be very complex "Objects" which track multiple values. In order to work with an Object, it's easier to pass around a reference (traditionally called a "pointer") to the Object. When we need to modify or use a value from that object, the program ultimately starts with the reference in order to find it.

In my case, I had written a function and assumed that the objects passed into it would be properly set up before this function was used. So I had to track down who was using this function, where their objects came from, and why those objects might not have been set up correctly.

Ultimately, the problem was that my new feature extraction code requires a new input parameter of a "test suite ID" which I had forgotten to add to the calling script. My code did not complain loudly enough when this argument was missing - it just rolled along with several uninitialized Objects instead!

To defend against this in code, I added a few blocks like this, which cause the program to preemptively exit a throw a "stack trace" for debugging when an object is null:
if (object == null) {
  throw new RuntimeException("Unexpected null object");
}
There are a couple of additional patterns/practices that can help avoid "NPEs", but I don't have time to implement them right now :)

IDR Series

1 comment: