Handling and Preventing Errors

Mistakes are an inevitable part of coding, and debugging is where the real problem-solving begins. This will inevitably lead to bugs. When you notice a bug you will, of course, spend some time figuring out what went wrong. There are two ways to tackle these bugs: detection and prevention.

The Bug


It started innocently enough: I had just finished creating the new model for the Hummingbird Moth. But when I loaded into the game and tried to spawn it, nothing happened. Something was wrong, and it was up to me to find out what.

The error log indicated the bug was down to a NullPointerException:

	Failure message: Butterfly Mod (butterflies) encountered an error during the common_setup event phase
		java.lang.NullPointerException: Cannot invoke "com.bokmcdok.butterflies.world.ButterflyData.butterflyIndex()" because "data" is null

I’ve seen this before. It’s telling me that the butterfly data doesn’t load for some reason. Unfortunately that reason could be absolutely anything. Looking at the JSON file, I couldn’t see anything wrong with the data I had provided.

{
  "breedTarget": "none",
  "diurnality": "nocturnal",
  "eggMultiplier": "normal",
  "entityId": "indianmeal",
  "extraLandingBlocks": "none",
  "habitat": "plains",
  "index": 45,
  "lifespan": {
    "butterfly": "short",
    "caterpillar": "medium",
    "chrysalis": "short",
    "egg": "short"
  },
  "plantEffect": "consume",
  "preferredFlower": "lily_of_the_valley",
  "rarity": "uncommon",
  "size": "tiny",
  "speed": "fast",
  "type": "moth"
}

I resorted to breakpoints in order to figure out what was going on. The relevant part of the code was in the ButterflyData class, in a method that loads and adds the butterfly data.

    /**
     * Load the butterfly data.
     * @param resourceManager The resource manager to use for loading.
     */
    public static void load(ResourceManager resourceManager) {
        Gson gson = new GsonBuilder().registerTypeAdapter(ButterflyData.class, new ButterflyData.Serializer()).create();

        // Get the butterfly JSON files
        Map<ResourceLocation, Resource> resourceMap =
                resourceManager.listResources("butterfly_data", (x) -> x.getPath().endsWith(".json"));

        // Parse each one and generate the data.
        for (ResourceLocation location : resourceMap.keySet()) {
            try {
                Resource resource = resourceMap.get(location);
                BufferedReader reader = resource.openAsReader();
                ButterflyData butterflyData = gson.fromJson(reader, ButterflyData.class);
                ButterflyData.addButterfly(butterflyData);
            } catch (IOException e) {
                LogUtils.getLogger().error("Failed to load butterfly data.", e);
            }
        }
    }

I used a conditional break point to look at the hummingbird moth data as it was being loaded in. When it got to calling addButterfly() I spotted the problem. You can actually see it in the JSON data above. entityId is incorrectly set to indianmeal. A simple copy/paste error where I forgot to change a value. This resulted in the Hummingbird Moth’s data overriding the Indianmeal Moth’s data instead of creating a new entry, as there is no checking for this in the addButterfly() method.

    /**
     * Create new butterfly data.
     * @param entry The butterfly data.
     */
    public static void addButterfly(ButterflyData entry)
            throws DataFormatException {
        ENTITY_ID_TO_INDEX_MAP.put(entry.entityId, entry.butterflyIndex);
        BUTTERFLY_ENTRIES.put(entry.butterflyIndex, entry);

        //  Recount the butterflies
        if (entry.type != ButterflyType.SPECIAL) {
            int total = 0;
            for (ButterflyData i : BUTTERFLY_ENTRIES.values()) {
                if (i.type == entry.type) {
                    ++total;
                }
            }

            if (entry.type == ButterflyType.BUTTERFLY) {
                NUM_BUTTERFLIES = total;
            } else if (entry.type == ButterflyType.MOTH) {
                NUM_MOTHS = total;
            }
        }
    }

I set the entityId to hummingbird and everything worked. The bug was fixed. But I wasn’t done yet. I needed a way to prevent this in the future.

While detecting this issue would save time, leaving it to human error for future occurrences isn’t an option. Prevention was the next logical step, ensuring this error, or anything like it, wouldn’t slip through the cracks again.

Detection


For the first solution, detecting the bug, I modified the addButterfly() method so it would throw an exception if an entry for the entityId or butterflyIndex already exists:

    /**
     * Create new butterfly data.
     * @param entry The butterfly data.
     */
    public static void addButterfly(ButterflyData entry)
            throws DataFormatException {
        if (ENTITY_ID_TO_INDEX_MAP.containsKey(entry.entityId)) {
            String message = String.format("Butterfly Data Entry for entity [%s] already exists.", entry.entityId);
            throw new DataFormatException(message);
        }

        if (BUTTERFLY_ENTRIES.containsKey(entry.butterflyIndex)) {
            String message = String.format("Butterfly Data Entry for index [%d] already exists.", entry.butterflyIndex);
            throw new DataFormatException(message);
        }

        // <snip>
    }

Next I modify the load() method so it catches this exception and logs the reason for the error

        // Parse each one and generate the data.
        for (ResourceLocation location : resourceMap.keySet()) {
            try {
                Resource resource = resourceMap.get(location);
                BufferedReader reader = resource.openAsReader();
                ButterflyData butterflyData = gson.fromJson(reader, ButterflyData.class);
                ButterflyData.addButterfly(butterflyData);
            } catch (DataFormatException | IOException e) {
                LogUtils.getLogger().error("Failed to load butterfly data.", e);
            }
        }

I tested this by setting the value back to indianmeal and ran the game, which gave me this output:

[21:38:50] [Server thread/ERROR] [co.bo.bu.wo.ButterflyData/]: Failed to load butterfly data.
java.util.zip.DataFormatException: Butterfly Data Entry for entity [indianmeal] already exists.

With this, I can see exactly why an error occurred, and I have a lot more information on how to fix it. This will definitely save some time, but things would be even better if the bug could never occur in the first place.

Prevention


Having the log output is useful, but if the bug never occurs then it would save a lot of time. I looked at the code generation script, written in Python, and I noticed that while the ButterflyData uses the entityId, the Python script uses the filename. This has never been a problem because I always set them both to the same value. Of course, we now know this can be a problem if they are ever mismatched.

I wanted to get rid of the value from the JSON and use the filename when parsing the data in code, but this led to a lot of issues involving the fact I had used a record and that the gson library requires methods to be implemented a certain way. There would be a way to do it, but it would involve a heavy rewrite of the way I’m parsing the data for butterflies.

Thankfully I realised that there’s a much simpler solution. We just ensure the entityId matches the filename when we generate the data:

            if "entityId" in json_data:
                json_data["entityId"] = entry

Adding in these two lines sets the value for us, and means that if I ever forget to set this value again, it will be fixed when I generate the data. By automating this small step in the Python script, I have eliminated a potential source of errors entirely. The best fixes are the ones that prevent you from making the mistake in the first place.

Final Thoughts


Debugging is as much about learning as it is about fixing errors. With this experience, I not only solved a tricky bug but also improved the stability and maintainability of the mod. And perhaps the most satisfying part of all is knowing that with these improvements, both the code and the coder become a little better.

Now that I have this bug dealt with, it’s time to get back to my work on the Hummingbird Moth.