Bugs are Fun!!

Fake Magazine Cover - D9's Nature Magazine with image of bugs having a party

Introduction / Audience

I stumbled across a bug in my code and I couldn't figure out what was causing it. After tracking it down, I felt this would make a good post to highlight a rare and interesting bug. I believe this is a known bug so this post is nothing groundbreaking, but I don't think it is very widely known.

The target audience is anyone from beginners to experts:

  • For beginners, you can learn how to approach finding a difficult bug in a logical methodical manner. It will also introduce you to Kotlin Playground if you haven't seen it before. 
  • For experts, you can test you knowledge and experience and see if you can identify the issue from the original code.

Warning: This post contains bugs and cute images of bugs.

The Bug

The bug was, as is usually the case, hidden in a large amount of code.

The Error

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "Node$Option.getCode()" because "it" is null

So a null-pointer error (NPE). Great. This should be easy. I'll basically look through my code for any double bangs (!! - not-null assertion operator) related to Node$Option.getCode().

The Process

I started off by looking for those double bangs, added some exception handling around areas of code that may have caused the issue, and couldn't quickly figure it out.

Hmmm. Interesting. This may be something new.

This was the point I started to find this fun. From what I could see, this wasn't the typical simple mistake I've dealt with a million times before. This actually energized me and made me more interested.

I eventually narrowed things down a little and copied the area of code that was causing the problem into a Raw Kotlin project (still in Android Studio). I couldn't reproduce it. Hmmm. This is strange.

Next, I went back to my original project, copied a large amount of code with a slightly different name and slowly started cutting things out while making sure the bug still occurred.

I must confess, I had already solved the issue at this point. With the help of AI and small sections of code, we were able to guess roughly what was causing it and fix the code; however, I was still curious about seeing exactly why it was happening.

After trimming everything down, I ended up with a minimal example of the code which I can share. I could of course cut this down further, but I want to keep some of the original feel of what it looked like. 

Minimal Example

sealed interface Node {
object OptionA: Node
object OptionB: Node

sealed interface Option {
val code: String

data object A : Option { override val code = "A" }
data object B : Option { override val code = "B" }

fun toAction() = when(this) {
A -> OptionA
B -> OptionB
}

companion object {
val childEntries = listOf(A, B)
}
}
}

You are missing around 90% of the original code here, but you still get a sense of what is going on. We have nested sealed interfaces, a node, options, toAction, and childEntries. There are two options with an associated code.

Notice: We have no double bangs anywhere, which was my initial expectation.

Experts, this is where you should stop reading. Can you identify the bug from this code alone without looking any further?

Image of bug on a computer thinking about code

The Next Step

Here's the actual code that causes the crash:

fun bugExample() {
println("Checking Node:")

println("A Code: ${Node.Option.A.code}")

Node.Option.childEntries.forEach {
println(" ${it.code}")
}

println("Codes checked")
}

Running this code will give you the error.

Kotlin Playground

To make things easier for you, I've added a Kotlin Playground link. This will take you straight to a working example that shows the bug in action.

Kotlin Playground Screenshot

Fun - Play!

So at a this point, you are free to play with the code above. Try removing sections or doing things differently. As we managed to cut down the original code by around 90%, any changes you make will likely lead to fixing the bug.

Play with the code in the playground before reading anything further. You can also copy the full code listing into your own IDE of choice if preferred.

Bug having fun

Findings and Fixes

The *bug* is not a Kotlin bug, it's actually by design. It relates to the initialization order. More information can be found here in youtrack.jetbrains. The bug is in my code as I was not aware of this issue.

"It's not a bug, because it works exactly as it is designed to work. As with any piece of design, it is an engineering compromise between many aspects (performance, usability, etc). However, it is a problem, because people sometimes run into it, get confused, sometimes spend quite a lot of time debugging, which means it is worth investigating what can be done to change the design to make it better." 
- Roman Elizarov (Project Lead for Kotlin at JetBrains at the time)

The Fix

The fix is really easy. Simply move the companion object from the Option interface up to the parent Node interface.
companion object {
val childEntries = listOf(A, B)
}
There are other ways to avoid the problem, but this is the real proper fix.

Other Fixes

When you were playing, you probably noticed some interesting behavior. There were a few things you could do that would fix this issue.
  • Removing the childEntries.forEach fixes it:
//            Node.Option.childEntries.forEach {
// println(" ${it.code}")
// }
That was the easiest one to find. The next two are a little harder.
  • Removing toAction fixes it:
//                fun toAction() = when(this) {
// A -> OptionA
// B -> OptionB
// }
  • Commenting out the println fixes it:
//            println("A Code: ${Node.Option.A.code}")
The last two were surprising *fixes*, but they were very obvious if you reduce the problem to the minimal example by cutting code.

When I was trimming down from the initial listing, I quickly came across these and had to leave them in for the bug to remain. I think this is the perfect practical example of why trimming down to a Minimal Reproducable Example (MRE) is a such a strong tool when dealing with difficult bugs.

Which Fix?

The reason I classify the three above as 'other fixes' is because they don't fix the core issue. The core issue was the initialization order: The companion initializes before its sibling objects are ready. The list snapshot is taken too early, nulls get frozen in.

The three above are workarounds that avoid the problem. The fix is to move the companion list to a parent. The parent companion has no initialization entanglement with the child objects, so it safely resolves them when the list is built.

Summary

We've looked at quite a rare Kotlin bug found organically in real code, played with fixes in Kotlin Playground, looked at the real fix, and generally had quite a good time!

I want to leave you with a couple of things to think about.
  • MRE - Making a Minimal Reproducible Example should be your go to if you get stuck on a bug. Cutting out 90% of the code that doesn't affect the issue makes solving it extremely simple. It can be difficult to cut down the code, but it is often far better than trying to fix one tiny bug in thousands of lines.
  • Have Fun - Fixing bugs should be fun. Look at it as a new puzzle or game. Programmers are, I believe, by nature very inquisitive and logical. If you can change your mindset to that of play and fun, programming becomes far more enjoyable. Try to become the person in your team everyone wants to consult about awkward bugs.
  • Understand - Understanding the issue is important. You can fix bugs with workarounds that will likely lead to errors in the future as all you really did we hack a solution that hides the problem. Just because the code works, it doesn't mean you fixed the issue. You don't want to get bitten by the same issue later in development.

Links

Kotlin Playground Examples

Related Blog Posts

Comments

Popular Posts