Solving the Bug: Room NullPointer Exception

  • Jahnavi jpg Janhavi Singh
6 minute read
Bugfinder 3 column

Once upon a time on a calm Sunday evening, I randomly opened my email inbox, only to find out that there had apparently been a large number of crashes on the Simple app. The crime: Sudden crashes in the app when the nurses tried to register a new patient. This in itself was enough to make the team's Monday a manic one, with all energies focused on solving this high-priority bug.

Sentry (our crash reporting tool) indicated that this was happening in our 25% rolled-out production release. Since we were not able to retrace or reproduce this at our end, we initially thought of chalking it off to a technical glitch. But as soon as we rolled the app out to 100%, the crash was officially reported by multiple users. And so Watson (Sentry) and the team (Sherlock) went to the crime scene to look around for possible cues.

Our first step in this case was to go to the patient registration screen in the app and try to reproduce the crash. If we could reproduce the crash in our machine, it could help us in debugging the problem.

It is to be noted here that we had recently added a type-ahead search feature to the patient's village address on the patient entry screen, i.e. "Division and Area" form field as shown here. But even after multiple tries, we were not able to reproduce this issue, and this led us to an uncharted territory and brought along a lot of questions.

Untitled

At this point, the only clue we had to investigate further was what our Watson - Sentry additionally handed to us—Sentry Logs. The message in the Sentry logs said - NullPointerException: Attempt to invoke interface method int `java.util.List.size()` on a null object reference

Untitled

It was clear that we were messing up with nullability somewhere, which led to the exception. We now knew at least what the problem was, and all that was left was to figure out what was causing it and then fix it.

We began tracing back our steps using the Sentry logs, and started from the exact point where the crash culprit was. In this case, it was in the ArrayAdapter.getCount method.

public class ArrayAdapter<T> extends BaseAdapter implements Filterable, 
ThemedSpinnerAdapter {
...
@Override
    public int getCount() {
        **return mObjects.size();**
    }
...
}

The approach we followed here was quite akin to what you'd do otherwise as well—question every single thing possible, till you can reach the answer. Almost like a modified, extended version of the 5 why analysis if you will.

"Why is the getCount method throwing a NullPointerException?"

One possible reason here could be that mObjects is null and size() is being called with a null object reference. So we checked where mObjects was being set. But the objects list that was being passed in the constructor was defined as non-nullable here, and so it wasn't possible to add null in mObjects.

public class ArrayAdapter<T> extends BaseAdapter implements Filterable, 
ThemedSpinnerAdapter {
...
**private List<T> mObjects;**
...
private ArrayAdapter(@NonNull Context context, @LayoutRes int resource,
            @IdRes int textViewResourceId, **@NonNull List<T> objects**, boolean objsFromResources) {
        mContext = context;
        mInflater = LayoutInflater.from(context);
        mResource = mDropDownResource = resource;
        **mObjects = objects**;
        mObjectsFromResources = objsFromResources;
        mFieldId = textViewResourceId;
    }
...
}

We then looked at the method we were using to pass in add elements to the mObjects list. But the collection array that was being passed in mObjects was defined as non-nullable here as well. This meant that the library methods were handling the nullability properly.

public class ArrayAdapter<T> extends BaseAdapter implements Filterable, 
ThemedSpinnerAdapter {
...
public void **addAll(@NonNull Collection<? extends T> collection)** {
        synchronized (mLock) {
            if (mOriginalValues != null) {
                mOriginalValues.addAll(collection);
            } else {
                **mObjects.addAll(collection);**
            }
            mObjectsFromResources = false;
        }
        if (mNotifyOnChange) notifyDataSetChanged();
    }
...
}

So, we backtracked even further to where this method was being called and where we were passing the collection to this method.

class EditPatientScreen(context: Context, attributeSet: AttributeSet) : RelativeLayout(context, attributeSet), EditPatientUi, HandlesBack {
...
	override fun setColonyOrVillagesAutoComplete(
      colonyOrVillageList: List<String>
  ) {
    colonyOrVillageEditText.setAdapter(villageTypeAheadAdapter)
    villageTypeAheadAdapter.clear()
    **villageTypeAheadAdapter.addAll(colonyOrVillageList)**
  }
...
}

Even here, the colonyOrVillageList was non nullable too.

We thought then that maybe we were doing something wrong in the colonyOrVillageEditText where we were setting the adapter, and to verify, we went back and took a quick look at the layout folder. However, everything looked as expected and the documented way of defining MaterialAutoCompleteTextView layout was being followed.

<com.google.android.material.textfield.MaterialAutoCompleteTextView
  android:id="@+id/colonyOrVillageEditText"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:hint="@string/patientedit_colony_or_village"
  android:imeOptions="flagNoFullscreen"
  android:inputType="textPostalAddress|textCapSentences" />

The next why we asked ourselves then was this: "Why was the colonyOrVillageList somehow passing a null even when it wasn't Nullable?" To get an answer to this, we decided to see where we were loading the colonyOrVillageList from.

class EditPatientViewRenderer(
    private val ui: EditPatientUi
) : ViewRenderer<EditPatientModel> {
  override fun render(model: EditPatientModel) {
...
	if (**model.hasColonyOrVillagesList**) {
	  **ui.setColonyOrVillagesAutoComplete(model.colonyOrVillagesList!!)**
	}
...

We checked if the model had the list or not and then sent a non null list to the setColonyOrVillagesAutoComplete method. But then we asked ourselves —"Why did we have a null in this list somehow, especially if this was clearly non nullable everywhere?"

We had to go further down the road and take a quick look at where were loading the list from in theEditPatientModel

@Query("""
          SELECT DISTINCT colonyOrVillage 
          FROM PatientAddress
          ORDER BY colonyOrVillage ASC
          """)
abstract fun getColonyOrVillages(): **List<String>**

We were primarily getting this list from the SQL query above, but it is to be noted that even this method returned a non nullable list of strings.

It was time to ask ourselves the last why question again and dig a little deeper:

"Why did we have a null in this list somehow, especially if this was clearly non nullable everywhere?"

"Where are we loading this list of colonyOrVillages from?"

To answer, we were loading it from the PatientAddress table. And so, we looked it up.

@Entity
@Parcelize
data class PatientAddress(
...
    **val colonyOrVillage: String?,**
...
) : Parcelable {

And guess what? The colonyOrVillage parameter in PatientAddress was NULLABLE, which meant that we were allowing to add null in place of colonyOrVillage in the PatientAddress table! We'd finally hit the jackpot and the road ahead seemed easier.

We tried to reproduce the bug by adding a few nulls in colonyOrVillage column, and a few others having some non nullable values. And voila, it worked! The solution was pretty evident, all we had to do was add a small null check in our SQL query that was and we were good to go.

@Query("""
      SELECT DISTINCT colonyOrVillage
      FROM PatientAddress
      **WHERE colonyOrVillage IS NOT NULL**
      ORDER BY colonyOrVillage ASC
      """)
abstract fun getColonyOrVillages(): List<String>

On a second thought, there was another question that popped up too—"If this was the case, why did we not get any errors when we allowed null items in a non nullable list? But what we realised was that Room does not do a good job in handling null safety errors as the Kapt for Room is not written in Kotlin. Because of that, it doesn't know whether the type we set it is a nullable or non-null to throw compilation warnings. And instead, it returns null for every possible value. So here even though we set the return type to List<String> , we still get a return value of List<String?>.

At the end of the day, this small journey of finding our criminal and solving the crime has brought to light a few basic things we can learn about debugging problems:

  • Error logs/stack traces are your Watson(best friend). They will help you get to the starting point from where you need to backtrack, until you find what you're looking for.
  • It's always a good idea to repeatedly ask yourself the why questions till you can get to the root cause of the bug. Remember: Machines are deterministic. Bugs are caused due to human errors.
  • Issues reported during partial rollouts should not be ignored or underestimated.