Hidden pitfalls when using Elvis operator

I guess many of us love Elvis, both the artist and the operator in Kotlin. But it can lead to some hidden pitfalls if you are not aware of how it works.

https://unsplash.com/photos/1LCzr14Ah5U

I only realized recently when Vladimir Zdravkovic put some code on twitter and ask us to guess what it’s printing:

https://twitter.com/vlazdra/status/1287366531987406848?s=20

I assumed a hidden puzzle but I could not see the issue. I could not see any reason why this would print anything but it does!
After thinking about the issue (Vladimir wrote an article about it) I found more and more cases where this could go wrong. But let me show you some code:

Kotlin’s null safety

I think most of us love the way we can easily write null safe code with Kotlin like this:

presenter?.onDestroy()

or

data?.let{ updateData(data) }

And it is super easy to add an alternative case:

data?.let{ updateData(data) } ?: run { showLoadingSpinner() }

Let me ask you something

Do you think the following code is basically the same as the above?

if (data != null) { 
updateData(data)
} else {
showLoadingSpinner()
}

I’m sure most of us do think they are equivalent. But what if I told you, it’s not?
The if/else is totally binary, it’s either-or. But with the Elvis operator, it might be both! To understand why we have to look closer to how it works.

Other than the else that belongs explicit to an if , the Elvis operator is not tied to a single ?. Remember, we can chain them:

someVariable?.someField?.doSomething()

if we now add the Elvis operator here, it will get executed depending on the expression to its left side:

someVariable?.someField?.doSomething() ?: run { doSomethingElse() }

so if any expression in there is null, the Elvis block will get called. It will finish evaluating everything on the left before checking if the operator is needed. This includes the last expression.

So this is depending on whatever doSomething() returns! If it is null, then the right side will be evaluated.

Or for our example:

data?.updateData(data) ?: run { showLoadingSpinner() }

If updateData returns null it will also show the loading spinner! This might not what you expect when thinking of it as an if/else.
You might think, this expression is safe but it might happen that both functions get called.

And this was what was happening in Vladimir's example:

https://dev.to/vlazdra/a-decompiled-story-of-kotlin-let-and-run-4k83

As the 2nd variable was null, it’s let was returning null and that lead to the invocation of the outer Elvis block.

So if you want the expected behavior, better do something explicitly as:

// Extension Function to handle else cases
fun <T> T?.whenNull(block: () -> Unit) = this ?: block()
data?.updateData(data).whenNull { handleNull() }

Remember, everything in Kotlin is resolving to some type including every expression. Remember, let is returning the result from your block (as does run). Stay aware! Be careful!

PS: Check out the decompiled code of the example above: https://dev.to/vlazdra/a-decompiled-story-of-kotlin-let-and-run-4k83

PPS: Special thanks to Lucas Nobile and Artur Latoszewski for additional inputs here, love the wisdom of the crowd 🤗